JavaScript实现防抖与节流

2020/3/26 JavaScript

参考: js防抖和节流 (opens new window)

# 函数防抖(debounce)

TIPS

触发高频事件在时间t后只会执行一次,如果在时间t内事件再次触发,则会重新计时。

/**
 * 函数防抖
 * @param {Function} func
 * @param {number} wait
 * @returns
 */
function debounce(func, wait) {
  let timerId = null;
  function debounced(...args) {
    const ctx = this;
    if (timerId) clearTimeout(timerId);
    timerId = setTimeout(() => {
      // 可在内部使用this
      func.apply(ctx, args);
    }, wait);
  }
  return debounced;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

如下,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件:

function handle() {
  console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
1
2
3
4
5

# 函数节流(throttle)

TIPS

当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

触发高频事件,且在时间t内只执行一次。

函数节流主要有两种实现方法:时间戳和定时器

# 时间戳实现

当高频事件触发时,第一次会立即执行(给scroll事件绑定函数与真正触发事件的间隔一般大于wait),而后再怎么频繁地触发事件,也都是每wait时间才执行一次。而当最后一次事件触发完毕后,事件也不会再被执行了。

function throttle(func, wait) {
  let prev = Date.now();
  return function (...args) {
    const ctx = this;
    const now = Date.now();
    if (now - prev >= wait) {
      func.apply(ctx, args);
      prev = Date.now();
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11

# 定时器实现

当触发事件的时候,我们设置一个定时器,再次触发事件的时候,如果定时器存在,就不执行,直到wait时间后,定时器执行执行函数,并且清空定时器,这样就可以设置下个定时器。当第一次触发事件时,不会立即执行函数,而是在wait秒后才执行。而后再怎么频繁触发事件,也都是每wait时间才执行一次。当最后一次停止触发后,由于定时器的wait延迟,可能还会执行一次函数。

function throttle(func, wait) {
  let timerId = null;
  return function (...args) {
    const ctx = this;
    if (!timerId) {
      timerId = setTimeout(() => {
        func.apply(ctx, args);
        timerId = null;
      }, wait);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12

# 时间戳+定时器

节流中用时间戳或定时器都是可以的。更精确地,可以用时间戳+定时器,当第一次触发事件时马上执行事件处理函数,最后一次触发事件后也还会执行一次事件处理函数。

在节流函数内部使用开始时间startTime、当前时间curTimewait来计算剩余时间remainTime,当remainTime < =0时表示该执行事件处理函数了(保证了第一次触发事件就能立即执行事件处理函数和每隔wait时间执行一次事件处理函数)。如果还没到时间的话就设定在remainTime时间后再触发(保证了最后一次触发事件后还能再执行一次事件处理函数)。当然在remainTime这段时间中如果又一次触发事件,那么会取消当前的计时器,并重新计算一个remainTime来判断当前状态。

/**
 * 函数节流
 * @param {Function} func
 * @param {number} wait
 * @param {boolean} leading 指定调用在节流开始前。
 * @param {boolean} trailing 指定调用在节流结束后。
 * @returns
 */
function throttle(func, wait, leading = true, trailing = true) {
  let timerId = null;
  let startTime = Date.now();
  let innerLeading = leading;
  function throttled(...args) {
    const curTime = Date.now();
    // 绑定事件时就已经有startTime了,所以第一次间隔正常情况下是大于wait的
    // 即 remainTime <= 0
    const remainTime = wait - (curTime - startTime);
    const ctx = this;
    clearTimeout(timerId);
    if (remainTime <= 0) {
      if (innerLeading) {
        func.apply(ctx, args);
      } else {
        innerLeading = true;
      }
      // 执行时重置开始时间
      startTime = Date.now();
    } else if (trailing) {
      timerId = setTimeout(() => {
        func.apply(ctx, args);
        // 定时器执行时重置开始时间
        startTime = Date.now();
      }, remainTime);
    }
  }
  return throttled;
}
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

如下,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。

function handle() {
  console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
1
2
3
4

# 总结

  • 函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在wait时间后触发函数,但是在wait时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
  • 函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

区别:函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。

比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

lodash-防抖 (opens new window)

/**
 * @param {Function} func 防抖目标函数
 * @param {number} [wait=0] 延迟时间毫秒数
 * @param {Object} [options={}] 配置参数
 * @param {boolean} [options.leading=false] 指定在延迟开始前调用
 * @param {number} [options.maxWait] 设置func允许被延迟的最大值
 * @param {boolean} [options.trailing=true] 指定在延迟结束后调用
 * @returns
 */
function debounce(func, wait, options = {}) {
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function');
  }
  const useRAF = !wait && wait !== 0 && typeof window.requestAnimationFrame === 'function';

  wait = +wait || 0;
  let lastArgs; // 保存上一次执行debounced的arguments
  let lastThis; // 保存上一次this
  let maxWait; // 设置func允许被延迟的最大值
  let result; // 函数func执行后的返回值,多次触发但未满足执行func条件时,返回result
  let timerId; // setTimeout生成的定时器句柄
  let lastCallTime; // 上一次调用debounce的时间
  let lastInvokeTime = 0; // 上一次执行func的时间
  let leading = false; // 指定在延迟开始前调用
  let maxing = false; // 是否设置了maxWait
  let trailing = true; // 指定在延迟结束后调用

  if (options && typeof options === 'object') {
    leading = !!options.leading;
    maxing = 'maxWait' in options;
    // maxWait和wait中的大值(如果 maxWait < wait,那maxWait就没有意义了)
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;
    trailing = 'trainling' in options ? !!options.trailing : trailing;
  }

  // 执行func,并返回func的执行结果
  function invokeFunc(time) {
    const args = lastArgs;
    const thisArg = lastThis;
    // 清空函数参数和this指向
    lastArgs = undefined;
    lastThis = undefined;
    // 保存函数执行的时间
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }

  // 开启定时器
  function startTimer(pendingFunc, milliseconds) {
    if (useRAF) {
      window.cancelAnimationFrame(timerId);
      return window.requestAnimationFrame(pendingFunc);
    }
    return setTimeout(pendingFunc, milliseconds);
  }

  // 取消定时器
  function cancelTimer(id) {
    if (useRAF) {
      window.cancelAnimationFrame(id);
      return;
    }
    clearTimeout(id);
  }

  // 前置边缘调用函数
  function leadingEdge(time) {
    lastInvokeTime = time;
    // 启动尾随边缘计时器
    timerId = startTimer(timerExpired, wait);
    // 是否前置边缘调用
    return leading ? invokeFunc(time) : result;
  }

  // 计算剩余调用的时间
  function remainingWait(time) {
    // 当前时间距离上一次调用 debounce 的时间差
    const timeSinceLastCall = time - lastCallTime;
    // 当前时间距离上一次执行 func 的时间差
    const timeSinceLastInvoke = time - lastInvokeTime;
    // 剩余等待时间
    const timeWaiting = wait - timeSinceLastCall;
    // 是否设置了最大等待时间
    // 是(节流):返回「剩余等待时间」和「距上次执行func的剩余等待时间」中的最小值
    // 否:返回剩余等待时间
    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  }

  // 判断此时是否立即执行func函数
  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime;
    const timeSinceLastInvoke = time - lastInvokeTime;
    // lastCallTime === undefined 第一次调用时
    // timeSinceLastCall >= wait 超过超时时间wait,处理事件结束后的那次回调
    // timeSinceLastCall < 0 当前时间 - 上次调用时间小于 0,即更改了系统时间
    // (maxing && timeSinceLastInvoke >= maxWait) 到达了maxWiat时间限制
    return lastCallTime === undefined
      || (timeSinceLastCall >= wait)
      || (timeSinceLastCall < 0)
      || (maxing && timeSinceLastInvoke >= maxWait);
  }

  // 定时器回调函数,表示定时结束后的操作
  function timerExpired() {
    const time = Date.now();
    // 是否需要立即执行
    if (shouldInvoke(time)) {
      trailingEdge(time);
      return;
    }
    // 否则重新计算剩余时间
    timerId = startTimer(timerExpired, remainingWait(time));
  }

  // 尾随边缘调用函数
  function trailingEdge(time) {
    timerId = undefined;
    // 只有lastArgs存在的时候才会调用,说明func至少经过一次防抖(debounced执行)
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = undefined;
    lastThis = undefined;
    return result;
  }

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

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

  function pending() {
    return timerId !== undefined;
  }

  function debounced(...args) {
    const time = Date.now();
    const isInvoking = shouldInvoke(time);
    lastArgs = args;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      // 判断是否立即执行
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      // 如果设置了最大等待时间,则立即执行 func
      // 1、开启定时器,到时间后触发 trailingEdge 这个函数。
      // 2、执行 func,并返回结果
      if (maxing) {
        // 在循环中处理调用
        timerId = startTimer(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait);
    }
    return result;
  }

  debounced.cancel = cancel;
  debounced.flush = flush;
  debounced.pending = pending;
  return debounced;
}
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180

lodash-节流 (opens new window)

/**
 * @param {Function} func 节流目标函数
 * @param {number} [wait=0] 延迟时间毫秒数
 * @param {Object} [options={}] 配置参数
 * @param {boolean} [options.leading=false] 指定在延迟开始前调用
 * @param {number} [options.maxWait] 设置func允许被延迟的最大值
 * @param {boolean} [options.trailing=true] 指定在延迟结束后调用
 * @returns
 */
function throttle(func, wait, options = {}) {
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function');
  }
  let leading = true;
  let trailing = true;

  if (options && typeof options === 'object') {
    leading = 'leading' in options ? !!options.leading : leading;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  return debounce(func, wait, {
    leading,
    maxWait: wait,
    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
最近更新: 2024年09月24日 17:59:19