Go to comments

渡一前端 微课 202501-2025-02

一、.gitignore 的匹配规则

.gitignore 文件用于排除 Git 跟踪的文件


1. 直接写名字

排出所有名字为 dist 的目录或文件,而且不管目录有多深,比如 /a/dist 也会被排除

dist

|- project

  |-a

  |  |- dist  被排除

  |

  |- dist  被排除


2. 加 / 斜杠

斜杠在开头,

表示当前目录下对应的目录或文件( 当前目录就是 .gitignore 所在的目录

/dist

|- project

  |-a

  |  |- dist 不会被排除

  |

  |- dist 根目录下的 dist 会被排除


斜杠在中间,

表示从当前目录出发,在 a 目录下的 dist 会被排除

a/dist

|- project

  |-a

  |  |- dist  被排除

  |

  |- dist  不会被排除


斜杠在末尾,

跟直接写 dist 查不多,且不管目录的深度,唯一的不同的是排除的是目录,不包括文件

dist/


3. * 通配符

* 匹配 0 个到多个字符,但不包括 / 斜杠

比如排除掉一些日志文件,无论那个目录下面的日志文件都会被排除

*.log

|- project

  |-a

  |  |- 2.log 被排除

  |

  |- 1.log 被排除


* 通配符和一些规则联合使用,比如只排除 logs 目录下所有 .log 文件

logs/*.log

|- project

  |-logs

  |  |- 2.log 被排除

  |

  |- 1.log 不会被排除


两个星号 **,

一个 * 不匹配斜杠,它只排除 Logs 目录下的子文件,

两个 ** 星号,可以匹配任何字符,同时也可以匹配斜杠,可以排除目录了

比如排除 logs 目录下面所有的 .log 文件,不管目录有多少深

logs/**/*.log

|- project

  |-logs

  |  |- sub

  |  |  |- 3.log 被排除

  |  |

  |  |- 2.log 被排除

  |

  |- 1.log 不会被排除


4. ? 号

只匹配名字是一个字符的 .log 文件,

比如 2.log 可以被排除,如果两个数字 31.log 就不能被排除

logs/**/?.log

|- project

  |-logs

  |  |- sub

  |  |  |- 31.log 不能被排除

  |  |

  |  |- 2.log 被排除

  |

  |- 1.log 不会被排除


如果两个 ?? 号匹配两个连续的字符


5. [] 中括号里写具体的匹配规则,也只匹配一个字符,

比如 [0-9] 表示匹配一个字符,同时必须是数字,a.log 不是数字不能被排除

logs/**/[0-9].log

|- project

  |-logs

  |  |- sub

  |  |  |- 31.log  不能被排除

  |  |

  |  |- 2.log  被排除

  |  |- a.log  不能被排除

  |

  |- 1.log  不会被排除


6. ! 感叹号,表示在前面的基础上,排除一些

比如,不跟踪 upload 目录下面所有用户上传的东西,不管目录有多深全部移除

/upload/**/*.*

|- project

  |-upload

    |- sub

    |  |- 1.zip

    |

    |- 1.jpg


但是这样会出一个问题,

由于 upload 下面全部被移除了,远程仓库那边不会建立这个 upload 目录,服务器那边可能会出一些问题,

常见的做法是在 upload 目录下面建立一个 .gitkeep 文件,文件名可以随意命名,习惯上用 gitkeep

|- project

  |-upload

    |- sub

    |  |- 1.zip

    |  

    |- .gitkeep  该文件的目的是不要被排除,远程仓库就要建立 upload 目录

    |- 1.jpg


这样 upload 目录下面除了 .gitkeep 以外全部被排除了,.gitkeep 起到了远程仓库建立 upload 目录的作用

/upload/**/*.*
!/upload/.gitkeep


一般在 .gitignore 的最后一行写上 !.gitkeep,保证在前面规则的基础上 .gitkeep 不会被排除

/upload/**/*.*
!.gitkeep


by:https://www.bilibili.com/video/BV1dkr3YMESN/


二、取整的区别

随机数函数里面,为什么要必须要用 Math.floor 取整呢?

function getRandom(x, y){
  return Math.floor(Math.random() * (y - x) + x);
}


parseInt 跟 Math.floor 的区别

parseInt 含义是向0取整,数轴的取整方向是 0

Math.floor 含义是向下取整,数轴方向是 -3

 -3,  -2.5,  -2,  -1,  0,  1,  2,  2.5,  3 

当取整的数字 2.5 是正数,这两个方法的方向是一致的,因此结果都是 2


如果是负数 -2.5

 -3,  -2.5,  -2,  -1,  0,  1,  2,  2.5,  3 

parseInt 向0取整,结果是 -2

Math.floor 向下取整,结果是 -3


比如,

产生随机范围是 -5 ~ 0 之间,0 是取不到的(因为 Math.random 产生的随机数范围是 [ 0, 1 ),1 是取不到的

function getRandom(x, y){
  return Math.random() * (y - x) + x;
}

const result = getRandom(-5, 0);
console.log(result);
console.log(Math.floor(result));


使用 Math.floor 取整的范围,从 -5 到 -1 每个数字的几率是一样的

 -5,  -4,  -3,  -2,  -1,  0 

-5 ~ -4  之间向下取整 -5 

-3 ~ -2  之间向下取整 -3 

-2 ~ -1  之间向下取整 -2 

-1 ~  0   之间向下取整 -1,0 取不到


parseInt() 是向 0 取整,

必须只有 Math.random 刚好为 0 的时候,经过该表达式的计算  Math.random() * (y - x) + x   才能取到 -5,极小几率才能出现

0 - -5 = -5

0  * -5 = 0

0 + -5 = -5


Math.round 是四舍五入,两头的几率小,中间的几率大,也不合适取整


by:https://www.bilibili.com/video/BV1Uw411i7s1/


三、为什么不建议用 js 计时器做动画

js 计时器做的运动动画

<div id="ball" style="position:absolute;background:darkorange;width:100px;height:100px;border-radius:50%;line-height:100px;text-align:center;color:#fff;"></div>

<script>

var x = 0;
var timer = null;

timer = setInterval(function(){
  x++;
  ball.style.left = x + "px";
  ball.innerText = x;
  if(parseInt(ball.style.left) >= 300){
    clearInterval(timer);
  }
}, 16);

</script>


_|__|__|__|__|__|__|_ >

浏览器会每隔一小段时间把页面画一遍,我们称之为“渲染”,每隔一段的时间画一遍的时间点叫“渲染帧”,通常情况下一秒钟有 60 帧,换句话就是一秒钟浏览器把页面画 60 遍


_|_A_|_A_|_A_|_A_|_A_|_ >

如果我们用 js 计时器做动画,可以精确的设置间隔时间,

保证两个渲染帧之间刚好夹一个 A 动画的操作,比如改变尺寸、位置等,

这样的好处是每一次改变,就可以不多不少的得到渲染了,但是这些都是在理想的情况下


_|___|_|_|__|__|___|__|_ >

实际的情况是渲染帧分部的没有那么平均,

这里面受到影响的因素就非常复杂了,可能是因为电脑配置、或系统卡了等,总之实际情况是渲染帧没有那么平均


_|_AA__|_|_|_A_|_A_|_A_A_|_A_| >

这样会导致有些渲染帧之间没有任何动画,我们叫空帧,空帧造成渲染帧的浪费,就是浏览器白画了一遍,

随着空帧而来的就是跳帧,就是在两个渲染帧之间出现了多次动画,

比如在两个帧之间改变两次位置,两处的代码是运行了,但是只渲染了第二次位置,第一次的位置根本没有画出来,此时页面上会明显感觉跳了一下,跳帧导致动画不连续


实际上情况更复杂,因为 js 计时器也不精准,这样的一来跳帧、空帧就会非常的严重,

所以如果用 js 计时器做动画,会导致动画看着好像有抖动的样子


那应该用什么方式做动画呢?

应该用 H5 新出的 api 叫 requestAnimationFrame 简称为 RAF


RAF 函数跟 setInterval 一样,参数也是传一个回调函数

requestAnimationFrame(function(){
  // 设置动画,例如改变位置
})


RAF 的意思是告诉浏览器,

在下一帧渲染的时候要运行 RAF 里面的回调函数,可以在该函数里面改变尺寸、位置等


但是 RAF 只会运行一次,要在每个渲染帧之间刚好夹一个动画,要用递归的方式调用

function raf(){
  requestAnimationFrame(function(){
    // 设置动画,例如改变位置
    raf(); // 继续设置下一帧
  })
}


在每一个动画设置完后,重新调用 raf 函数重新设置下一帧夹的动画,形成这样的效果

_|_A__|_A|_A_|_A|A_|__A_|_A_|->


这个效果好处是不像定时器有时间间隔,它没有时间间隔,它永远是跟着渲染帧走,保证每个渲染帧之间刚好有一个动画操作,没有空帧和跳帧动画非常平滑

var x = 0;

function raf(){
  requestAnimationFrame(function(){
    x++;

    if(parseInt(ball.style.left) >= 300){
      return;
    }
 
    ball.innerText = x;
    ball.style.left = x + "px";
    raf(); // 继续设置下一帧

  });
}

raf();


思考题

以你的经验,哪些动画css实现不了,必须要用 js 动画


by:https://www.bilibili.com/video/BV1gu411b7bS/


四、什么是高阶函数

以下两个条件,满足任何一个条件的都是高阶函数

1. function as arguments 函数作为参数

2. returns a function  返回一个函数


我们每一天甚至每一个 js 文件里面都用过高阶函数,

比如数组里面的 map、filter、reduce 等方法都是高阶函数,因为要传一个函数作为参数

const arr = [12, 3, 544];

const result = arr.map(a => a+2);

console.log(result); //  [14, 5, 546]


平时常用的 bind 方法,它会返回一个新的函数,所以它也是一个高级函数

function a(){
  console.log(this);
}

const b = a.bind({});

b();


高阶函数到底有什么用呢?

如果没有高阶函数,js 代码都不知道该怎么写了,比如,

计时器也是高阶函数,它第一参数也是函数。

事件注册的方法也是高级函数,它的其中一个参数也是函数

setTimeout(()=>{
  
}, timeout)

dom.addEventListener('click', ()=>{
  
})


高阶函数的本质,

在函数式编程里面,它把每一个函数看做是一个运算,认为函数的本质就是运算,

给函数一些东西,函数可以算出一些东西,

而高阶函数表达的是运算的“缺失”和“延续”


缺失的意思

比如函数 map 的逻辑

map 要返回一个新数组 result

需要循环原数组,把原数组的每一项经过一套规则,转换成一个新的值,

然后把新的值放到 result 数组里

function map(){
  const result = [];
  for(let i=0;i<原数组.length;i++){
    原数组[i] -> 新的值 // 这里缺失了一个运算
    result.push(新的值)
  }
  return result; 
}


这个 map 函数的运算过程中有一块  原数组[i] -> 新的值  的拼图是缺失的,这里需要一个把原数组的值转换成新值的运算,

这个运算本质是一个函数,该函数需要作为参数传进去把拼图补齐

function map(fn){
  const result = [];
  for(let i=0;i<原数组.length;i++){
    const 新的值 = fn(原数组[i]); // 通过 fn 的运算拿到新的值
    result.push(新的值)
  }
  return result; 
}

当有一个函数作为参数传进来的时候,就意味着有一个运算的缺失,需要用高阶函数来进行表达


延续的意思,

比如手写一个 bind 函数,

参数传一个 this 的指向,然后生成一个绑定这个 this 指向的函数 fn,

bind 函数只负责生成 fn 函数,不管 fn 函数什么时候运行,而是把 fn 函数作为返回值返回

function bind(thisArg){ 
  const fn = 生成一个绑定了this指向的函数
  return fn; // 返回绑定this的函数
}

function a(){

}

const b = a.bind({}); // b就是返回值fn函数

b();

将来调用 bind 拿到的返回值,就是拿到了 bind 里面生成的 fn 函数的功能,

也就是说 bind 函数虽然执行完了,但是它里面 fn 的功能会延续到外面在将来的某一个时刻发挥作用,这就是运算的延续


总结

在 js 里面看到的各种不一样的高阶函数,但是它们的底层逻辑都是这样的

1. 在运算缺失的时候,需要函数作为参数,

2. 需要对运算进行延续的时候,我们会使用函数的返回值,


袁老师说

学习高阶函数最重要的是,高阶函数在实际工作中到底起到一个什么样的作用,

根据工作中的实际情况,自己去编写高阶函数,

高阶函数常常会出现在公共的代码、公共的模块里面,


通过如何编写一个高阶函数,学习高阶函数在实际开发中的各种应用,

从而逐渐的找的一种感觉,这种感觉既是编写高阶函数的感觉,同时也是抽离公共代码的感觉,

这中感觉对将来代码的质量和工作的效率有成倍的提升


by:https://www.bilibili.com/video/BV1my4y1P77K


五、封装动画函数(高阶函数的应用)

高阶函数的应用 - 封装动画函数


数字的变化不是 css 样式,所以 css 动画无能为力了,

或者在 canvas 里面做一些动画效果,css 也是无能为力的


封装一个 animation 函数

我们希望不管是什么样的动画效果,只要需要 js 动画,就可以用这个函数来完成


接下来

我们思考一下,动画的本质是什么?

动画的本质就是从一段时间只内,从一个数字变化到另一个数字,

元素的宽高是数字、元素的颜色(RGB)是数字


duration 时间单位为毫秒,从 from 变化到 to

function animation(duration, from, to){

}


dis 获取变化的距离

speed 距离除以时间得到变化的速度

加下来

startTime 记录开始的时间

value 当前值,当前的值从起始值 from 始的,每隔一小段时间对 value 当前值进行变化

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>高阶函数的应用-封装动画函数</title>
<style>
.btn{background:dodgerblue;color:#fff;outline:none;border:none;padding:10px 20px;border-radius:10px;}
</style>
</head>
<body>

<label>价格:2999.00</label>
<button class="btn">打折</button>

<script>

const btn = document.querySelector(".btn");
const label = document.querySelector("label");

btn.onclick = function(){
  // 3秒钟内,从2999变到299
  animation(3000, 2999, 299);
}

// 动画函数
function animation(duration, from, to){
  const dis = to - from;
  const speed = dis / duration;
  const startTime = Date.now();
  let value = from;
  console.log(value); // 2999
}

</script>
</body>
</html>

现在打印 value,当前的值为 2999


下面每隔一小段时间让它 value 进行变化,把变化写成一个辅助函数 _run

1. 每调用一次 _run 函数,它都会根据当前的信息,算出一个新的值,重新给 value 赋值

2. 然后下一次渲染的时候(requestAnimationFrame),重新调用 _run 函数

function animation(duration, from, to){
  const dis = to - from;
  const speed = dis / duration;
  const startTime = Date.now();
  let value = from;
  console.log(value); // 2999

  function _run(){
  }

  requestAnimationFrame(_run);
}


_run 函数里面写什么呢

1. now 首先获取当前的时间

2. time = now - startTime  然后让当前时间和起始时间相减,得到从起始时间到现在流逝的时间(计算 value 的值步骤一)

3. d = time * speed  流逝的这段时间运动了多少?用流逝的时间乘速度(计算 value 的值步骤二)

4. 然后用起始值 from 加上逝去时间运动的距离 d,等于新的当前值  value(计算 value 的值步骤三)

function animation(duration, from, to){
  const dis = to - from;
  const speed = dis / duration;
  const startTime = Date.now();
  let value = from;
  console.log(value);

  function _run(){
    const now = Date.now() // 首先获取当前的时间
    const time = now - startTime; // 从起始时间到现在流逝的时间 

    // 流逝的这段时间运动了多少距离呢?
    // 用流逝的时间 * 速度,可以算出变化的距离
    const d = time * speed;
    // 然后用起始值加上变化的距离
    value = from + d;
    console.log(value);
  }

  requestAnimationFrame(_run);
}


接下来每隔一小段时间调用 _run,就可以不断的去修改 value

function animation(duration, from, to){
  const dis = to - from;
  const speed = dis / duration;
  const startTime = Date.now();
  let value = from;
  console.log(value);

  function _run(){
    const now = Date.now()
    const time = now - startTime;
    const d = time * speed;
    value = from + d;
    console.log(value);
    // 做法非常简单就是递归,用requestAnimationFrame运行_run函数
    requestAnimationFrame(_run);
  }

  requestAnimationFrame(_run);
}


如何终止递归呢?

当逝去的时间超过了 duration 的时候就停止,停止的时候把 value 设置为目标值 to

function animation(duration, from, to){
  const dis = to - from;
  const speed = dis / duration;
  const startTime = Date.now();
  let value = from;
  console.log("初始值",value);

  function _run(){
    const now = Date.now()
    const time = now - startTime;
    // 停止递归
    if(time >= duration){
      value = to;
      return;
    }
    const d = time * speed;
    value = from + d;
    console.log("变化",value);
    // 停止后以后,可以在这里获取label元素,然后修改内容
    label.innerText = "价格:"+value;
    requestAnimationFrame(_run);
  }
  
  requestAnimationFrame(_run);
}


这个简单的动画函数就差不多了,

实际上一个完整的动画函数是非常复杂的,要考虑很多因素,

比如,

不需要现在这样频繁的触发数字,在整数位置时候触发就可以了,

现在是匀速变化,开发中可能需使用贝塞尔曲线函数


现在,

这个简单的动画函数和高阶函数还没有什么关系,

这是一个通用的函数,可以用在页面的任何地方,

比如,改变价格、倒计时、还可以可控制元素的移动,这些都是 css 数字的变化,

既然是通用的函数,就不能在函数内部跟具体的某一个元素绑在一起


虽然可以把元素当做参数传进来

 animation(duration, from, to, 元素) 

但是依然不能实现,改变元素的内容、位置、透明度、颜色等通用的需求


其实,

这个问题的本质是 animation 函数里面缺失一个运算,

我们拿到 value 变化的值,拿这个值做什么的运算


可以这样,

使用 animaiton 函数的的时候,再多传一个参数 onProgress,该参数是一个回调函数,

在该回调函数里面处理 value 的运算,这个拼图就完整了


onProgress(val) 回调函数接收一个参数,把 value 新的值作为参数传给该回调函数

const btn = document.querySelector(".btn");
const label = document.querySelector("label");

btn.onclick = function(){
  // 第四个参数是一个函数,该函数弥补了animation里面缺失的那一块拼图
  animation(3000, 2999, 299, (val) => {
    label.textContent = `价格:${val.toFixed(2)}`; // 当拿到val后,修改价格的变化
  });
}


function animation(duration, from, to, onProgress){
  const dis = to - from;
  const speed = dis / duration;
  const startTime = Date.now();
  let value = from;
  onProgress(value); // 补齐缺失的运算
  
  function _run(){
    const now = Date.now()
    const time = now - startTime;
    if(time >= duration){
      value = to;
      // console.log("value",value);
      onProgress(value); // 补齐缺失的运算
      return;
    }
    const d = time * speed;
    value = from + d;
    onProgress(value); // 补齐缺失的运算
    // console.log("变化",value);
    requestAnimationFrame(_run);
  }
  
  requestAnimationFrame(_run);
}


通过高阶函数,

保证了 animation 函数的通用性,保证适应各种 js 动画的变化

比如,改变元素的位置,在回调函数里面修改一下代码 label.style.left = val + "px" 就可以


by: https://www.bilibili.com/video/BV1vy4y1P7ax/








Leave a comment 0 Comments.

Leave a Reply

换一张