# 防抖与节流

作者:魏聪 时间:2020-10-12

# 1. 前言

我们 JavaScript 中有一些事件,比如浏览器的 resize、scroll 事件,鼠标的 mousemove、mouseover 事件以及输入框的 keypress、keyup 事件,它们在触发的时候会不断调用事件绑定的回调函数,极大的浪费资源,降低前端性能。为了优化用户体验,我们需要对这类事件进行调用次数的限制。我们可以使用防抖与节流来降低事件的触发频率。

# 2. 防抖

# 2.1 定义

当连续触发事件时,事件被触发 N 秒后才执行回调,否则如果在这 n 秒内又被触发,则重新计时

例:鼠标移动事件 正常情况:从开始监听到鼠标不再移动打印出无数次 防抖处理:只在鼠标不再移动后 N 秒打印出鼠标最后停止的位置(N 秒为人为设置)

# 2.2 为什么要防抖?

在某一函数在短时间内被连续触发导致占用大量性能使得系统卡顿,而我们并不真正需要连续触发这个函数。我们需要的是连续触发中最后一次的结果,这时我们使用防抖来优化函数的触发,达到减少占用性能,防止连续触发导致的卡顿。

# 2.3 防抖的实现

// 防抖
function debounce(fun, delay) {
  return function (args) {
    let that = this;
    let _args = args;
    clearTimeout(fun.id);
    fun.id = setTimeout(function () {
      fun.call(that, _args);
    }, delay);
  };
}
1
2
3
4
5
6
7
8
9
10
11

# 2.4 示例

移动鼠标,更新数字,数字记录调用事件回调次数

# 2.4.1 正常调用

每次移动都会调用回调。

// 容器
const container = document.getElementById("container");
// 数字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = callback;
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

正常调用

# 2.4.2 防抖调用

每次移动间隔超过 1000ms 才会调用回调,反之重新计算时间。

// 容器
const container = document.getElementById("container");
// 数字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = dobounce(callback, 1000);
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

防抖实现

# 2.4.3 防抖优化

首次调用立即执行

// 防抖
function debounce(fun, delay, immediate) {
  return function (args) {
    let that = this;
    let _args = args;
    clearTimeout(fun.id);
    if (immediate) {
      // 如果已经执行过,不再执行
      const callNow = !fun.id;
      fun.id = setTimeout(function () {
        fun.id = null;
      }, delay);
      if (callNow) fun.call(that, _args);
    } else {
      fun.id = setTimeout(function () {
        fun.call(that, _args);
      }, delay);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

防抖优化

# 3.节流

# 3.1 定义

当连续触发事件时,每隔 N 时间,触发一次函数

例:监听鼠标在屏幕中的位置并打印,连续不断的移动鼠标一段时间,获取鼠标的移动轨迹 正常情况:从开始监听到鼠标不再移动打印出无数次,(轨迹最为精细) 节流处理:当我们不需要这样精确的轨迹时,每隔 N 秒触发一次函数打印鼠标位置,得到较为粗糙的鼠标轨迹(N 秒为人为设置)

# 3.2 为什么要节流?

同样的,也是为了防止在某一函数在短时间内被连续触发导致占用大量性能使得系统卡顿,而我们并不真正需要连续触发这个函数。我们需要的是稳定的每隔一段时间触发一次,这时我们使用节流来优化函数的触发,达到减少占用性能,防止连续触发导致的卡顿。

# 3.3 节流的实现

// 节流
function throttle(fun, delay) {
  let last, deferTimer;
  return function (args) {
    let that = this;
    let _args = arguments;
    let now = +new Date();
    // 保证第一次执行
    if (last && now < last + delay) {
      clearTimeout(deferTimer);
      deferTimer = setTimeout(function () {
        last = now;
        fun.apply(that, _args);
      }, delay);
    } else {
      last = now;
      fun.apply(that, _args);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3.4 示例

移动鼠标,更新数字,数字记录调用事件回调次数

# 3.4.1 节流调用

首次调用立即执行,每隔 1000ms 调用一次。

// 容器
const container = document.getElementById("container");
// 数字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = throttle(callback, 1000);
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

节流调用

# 4. 附加

lodash 的防抖与节流实现

// 防抖
function debounce(func, wait, options) {
  var lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime,
    lastInvokeTime = 0,
    leading = false,
    maxing = false,
    trailing = true;

  if (typeof func != "function") {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  wait = toNumber(wait) || 0;
  if (isObject(options)) {
    leading = !!options.leading;
    maxing = "maxWait" in options;
    maxWait = maxing
      ? nativeMax(toNumber(options.maxWait) || 0, wait)
      : maxWait;
    trailing = "trailing" in options ? !!options.trailing : trailing;
  }

  function invokeFunc(time) {
    var args = lastArgs,
      thisArg = lastThis;

    lastArgs = lastThis = undefined;
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }

  function leadingEdge(time) {
    lastInvokeTime = time;
    timerId = setTimeout(timerExpired, wait);
    return leading ? invokeFunc(time) : result;
  }

  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime,
      timeWaiting = wait - timeSinceLastCall;

    return maxing
      ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  }

  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime;
    return (
      lastCallTime === undefined ||
      timeSinceLastCall >= wait ||
      timeSinceLastCall < 0 ||
      (maxing && timeSinceLastInvoke >= maxWait)
    );
  }

  function timerExpired() {
    var time = now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  function trailingEdge(time) {
    timerId = undefined;
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }

  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(now());
  }

  function debounced() {
    var time = now(),
      isInvoking = shouldInvoke(time);

    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        clearTimeout(timerId);
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}

// 节流
function throttle(func, wait, options) {
  var leading = true,
    trailing = true;

  if (typeof func != "function") {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  if (isObject(options)) {
    leading = "leading" in options ? !!options.leading : leading;
    trailing = "trailing" in options ? !!options.trailing : trailing;
  }
  return debounce(func, wait, {
    leading: leading,
    maxWait: wait,
    trailing: trailing,
  });
}
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

# 5. 参考链接

lodash 的实现 可视化防抖与节流 防抖