防抖
你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。
这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。
PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。
我们先来看一个袖珍版的防抖理解一下防抖的实现:
1  | // func是用户传入需要防抖的函数  | 
这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用,一般的防抖会有immediate选项,标识是否立即调用,这两者的区别,举个例子来说:
- 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用 延迟执行 的防抖函数,它总是在一连串(间隔小于wait的)函数出发之后调用。
 - 例如用户给 某个github项目点star的时候,我们希望用户点的第一下的时候就去调用接口,并且成功之后改变star按钮的样式,用户就可以立马得到反馈是否star成功了,这个情况适用 立即执行 的防抖函数,它总是第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
 
下面实现了一个带有立即执行选项的防抖函数
1  | function now () {  | 
整体函数的实现并不难,总结一下:
- 对于按钮防点击来说的实现: 如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行,一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点lei了,定时器时间到,定时器重置为null,就可以再次点击了。
 - 对于延迟执行的函数来说:清楚定时器,如果是延迟调用就调用函数
 
节流
防抖和节流本质是不一样的,防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62/**
  * underscore 节流函数,返回函数连续调用时,func执行频率限定为 次/wait
  * @param {function} func 回调函数
  * @param {number} wait 表示时间窗口的间隔
  * @param {object} options 如果想忽略开始函数的调用,传入 {leading: false}
  *                          如果想忽略结尾函数的调用,传入{trailing: false}
  *                          两者不能共存,否则函数不能执行
  *@return {funciton}        返回客户调用函数
  */
_.throttle = function (func, wait, options) {
  var context, args, result
  var timeout = null
  // 之前的时间戳
  var previous = 0
  if (!options) options = {}
  var later = function () {
    // 如果设置了 leading 就将 previous 设为 0
    // 用于下面函数的第一个 if 判断
    previous = options.leading === false ? 0 : _.now()
    // 置空一是为了防止内存泄露 二是为了下面的定时器判断
    timeout = null
    result = func.apply(context, args)
    if (!timeout) context = args = null
  }
  return function () {
    // 获取当前时间戳
    var now = _.now()
    // 首次进入前者肯定为 true
    // 如果需要第一次不执行函数
    // 就将上次时间戳设为当前的
    // 这样在接下来计算 remaining 的值时会大于0
    if (!previous && options.leading === false) previous = now
    // 计算剩余时间
    var remaining = wait - (now - previous)
    context = this
    args = arguments
    // 如果当前调用已经大于上次调用时间 + wait
    // 或者用户手动调了时间
    // 如果设置了 trailing,只会进入这个条件
    // 如果没有设置 leading,那么第一次会进入这个条件
    // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
    // 其实还是会进入的,因为定时器的延时
    // 并不是准确的时间,很可能你设置了2秒
    // 但是他需要2.2秒才触发,这时候就会进入这个条件
    if (remaining <= 0 || remaining > wait) {
      // 如果存在定时器就清理掉否则会调用二次回调
      if (timeout) {
        clearTimeout(timeout)
        timeout = null
      }
      previous = now
      result = func.apply(context, args)
      if (!timeout) context = args = null
    } else if (!timeout && options.trailing !== false) {
      // 判断是否设置了定时器和 trailing
      // 没有的话就开启一个定时器
      // 并且不能不能同时设置 leading 和 trailing
      timeout = setTimeout(later, remaining)
    }
    return result
  }
}
