例如 scroll、input 等事件频繁触发时,可能造成性能问题,若事件处理时使用了异步方法(例如 ajax 请求),还有可能导致结果错误,例如输入框输入时自动 ajax 请求查询数据,若不进行处理,则会频繁触发 ajax 请求,且显示的查询结果可能不是当前输入内容的返回结果。

概念

  • 设定一个周期,期间不执行动作
  • 前缘 leading 和延迟 trailing
    • 前缘:执行动作后再开始周期
    • 延迟:周期结束后再执行动作
  • 防抖 debounce 和节流 throttle
    • 防抖:若期间动作又被触发,则重新设定周期
    • 节流:周期固定


应用场景

防抖 debounce

简单来说,就是在一段时间大量频繁触发事件时,只在最后一次事件触发时执行动作。

  1. 比如输入框 input onchange,输入查询字符串时实时搜索相应结果,只需要查询最后输入完成的字符串。
  2. 又或者窗口 resize 后重新计算页面某一元素尺寸 width/height,只需要 resize 完成后进行计算。

    节流 throttle

    而有些情况下则需要限制频率的同时多次触发,而非只是最后一次。
    例如最常见的监听 scroll 事件并动态加载某一元素(例如返回顶部按钮,或是懒加载图片等等)

lodash 实现

防抖 debounce

  1. /**
  2. * 部分代码有一定简化
  3. * fn: 执行函数
  4. * wait: 周期时间
  5. * leading: true 表示前缘执行,false 表示延迟执行
  6. * maxWait: 如果设置了,就表示固定 maxWait 时间后一定执行
  7. */
  8. function debounce(fn, wait, leading = false, maxWait) {
  9. let result;
  10. let lastThis;
  11. let lastCallTime;
  12. let lastInvokeTime = 0;
  13. let timerId;
  14. if (maxWait !== undefined) maxWait = Math.max(maxWait || 0, wait);
  15. // 开始周期
  16. function leadingEdge(time) {
  17. lastInvokeTime = time;
  18. timerId = setTimeout(timerExpired, wait);
  19. // 若设置了前缘 leading,则立即执行一次 fn
  20. return leading ? invokeFunc(time) : result;
  21. }
  22. // 结束周期
  23. function timeExpired() {
  24. const time = Date.now();
  25. if (shouldInvoke(time)) {
  26. timerId = undefined;
  27. if (!leading && lastArgs) {
  28. // lastArgs 表示 fn 至少 debounced 了一次
  29. return invokeFunc(time);
  30. }
  31. lastArgs = lastThis = undefined;
  32. }
  33. return result;
  34. }
  35. // 执行动作
  36. function invokeFunc(time) {
  37. const args = lastArgs;
  38. const thisArg = lastThis;
  39. lastArgs = lastThis = undefined;
  40. lastInvokeTime = time;
  41. result = fn.call(thisArg, ...args);
  42. return result;
  43. }
  44. // 判断是否处于可执行阶段
  45. function shouldInvoke(time) {
  46. const timeSinceLastCall = time - lastCallTime;
  47. const timeSinceLastInvoke = time - lastInvokeTime;
  48. return (
  49. lastCallTime === undefined // 第一次调用
  50. || timeSinceLastCall >= wait // 已经过了一个周期不需要等待了
  51. || timeSinceLastCall < 0 // shouldInvoke 调用已经过时了
  52. || (maxWait && timeSinceLastInvoke >= maxWait) // 到了固定周期执行时间了
  53. );
  54. }
  55. function debounced(...args) {
  56. const time = Date.now();
  57. lastThis = this;
  58. lastArgs = args;
  59. lastCallTime = time;
  60. if (shouldInvoke(time)) {
  61. if (timerId === undefined) {
  62. // 没有等待的周期
  63. return leadingEdge(lastCallTime);
  64. }
  65. if (maxWait) {
  66. // 有设置固定强制执行周期时间时,强制调用
  67. timerId = setTimeout(timeExpired, wait);
  68. return invokeFunc(lastCallTime);
  69. }
  70. }
  71. if (timerId === undefined) {
  72. // 第一次调用
  73. timerId = setTimeout(timeExpired, wait);
  74. }
  75. return result;
  76. }
  77. return debounced;
  78. }

节流 throttle

  1. function throttle(fn, wait, leading) {
  2. // throttle 和 debounce 的差别在于固定周期执行,即 maxWait 设置为 wait
  3. return debounce(fn, wait, leading, wait);
  4. }