在开发中会经常遇到一些需要频繁触发的事件的场景。

  1. window 的 resize、scroll
  2. mousedown、mousemove
  3. keyup、keydown

……

demo

  1. // HTML 代码
  2. <body>
  3. <div id="container">
  4. </div>
  5. </body>
  6. // CSS 代码
  7. <style>
  8. #container {
  9. width: 100%;
  10. height: 200px;
  11. line-height: 200px;
  12. text-align: center;
  13. color: #fff;
  14. background-color: #444;
  15. font-size: 30px;
  16. }
  17. </style>
  18. // javascript 代码
  19. <script>
  20. let count = 1;
  21. let container = document.getElementById("container");
  22. function getUserAction() {
  23. container.innerHTML = count++;
  24. }
  25. container.onmousemove = getUserAction;
  26. </script>

debounce.gif

当鼠标滑过时会频繁地触发事件,这个 demo 比较简单,所以浏览器可以完全反应过来,但是如果遇到比较复杂回调函数呢?如 ajax 请求。这样的话就未必能应付过来。

所以需要限制回调函数的触发频率。防抖就是其中一种方法。

防抖的原理:尽管触发事件,但是一定在事件触发 n 秒后才执行回调函数,如果一个事件触发的 n 秒内又触发了这个事件,那么又以这个新的事件的时间为准,n 秒后才执行,总之,就是在触发事件的 n 秒内不再触发事件。

第一版

  1. // 第一版
  2. function debounce(func, wait) {
  3. let timeout;
  4. return function () {
  5. clearTimeout(timeout);
  6. timeout = setTimeout(func, wait);
  7. };
  8. }

以开头 demo 为例

  1. container.onmousemove = debounce(getUserAction, 1000);

不管触发多少次都会在下一次触发时取消上一次的 timeout。

debounce1.gif

this 和 event

解决 this 和 event。

  1. // 第二版
  2. function debounce(func, wait) {
  3. let timeout;
  4. return function () {
  5. const context = this, args = arguments;
  6. clearTimeout(timeout);
  7. timeout = setTimeout(function () {
  8. func.apply(context, args);
  9. }, wait);
  10. };
  11. }

立即执行

新需求:不希望等到事件停止触发后才执行,希望能立即执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。

  1. // 第三版
  2. function debounce(func, wait, immediate) {
  3. let timeout;
  4. return function () {
  5. const context = this, args = arguments;
  6. if (timeout) clearTimeout(timeout);
  7. if (immediate) {
  8. // 如果已经执行过,不再执行
  9. let callNow = !timeout;
  10. timeout = setTimeout(function () {
  11. timeout = null;
  12. }, wait);
  13. if (callNow) func.apply(context, args);
  14. } else {
  15. timeout = setTimeout(function () {
  16. func.apply(context, args);
  17. }, wait);
  18. }
  19. };
  20. }

debounce2.gif

返回值

执行的函数可能会有返回值,所以需要返回函数的执行结果,但是当 immediate 为 false 时,因为使用了 setTimeout,所以返回值是 undefined,所以 immediate 为 true 时,才有返回值。

  1. // 第四版
  2. function debounce(func, wait, immediate) {
  3. let timeout, result;
  4. return function () {
  5. const context = this, args = arguments;
  6. if (timeout) clearTimeout(timeout);
  7. if (immediate) {
  8. // 如果已经执行过,不再执行
  9. let callNow = !timeout;
  10. timeout = setTimeout(function () {
  11. timeout = null;
  12. }, wait);
  13. if (callNow) result = func.apply(context, args);
  14. } else {
  15. timeout = setTimeout(function () {
  16. func.apply(context, args);
  17. }, wait);
  18. }
  19. return result;
  20. };
  21. }

cancel

新需求:希望能取消 debounce 函数,比如 debounce 的时间间隔是 10 秒钟,immediate 为 true 时,这样只能再等上 10 秒才能重新触发执行,现在需要一个功能当触发这个功能后,取消防抖,这样再去触发,就可以立即执行。

  1. // 第五版
  2. function debounce(func, wait, immediate) {
  3. let timeout, result;
  4. const debounced = function () {
  5. const context = this, args = arguments;
  6. if (timeout) clearTimeout(timeout);
  7. if (immediate) {
  8. // 如果已经执行过,不再执行
  9. let callNow = !timeout;
  10. timeout = setTimeout(function () {
  11. timeout = null;
  12. }, wait);
  13. if (callNow) result = func.apply(context, args);
  14. } else {
  15. timeout = setTimeout(function () {
  16. func.apply(context, args);
  17. }, wait);
  18. }
  19. return result;
  20. };
  21. debounced.cancel = function () {
  22. clearTimeout(timeout);
  23. timeout = null;
  24. };
  25. return debounced;
  26. }

以 demo 为例

  1. let count = 1;
  2. const container = document.getElementById("container");
  3. function getUserAction() {
  4. container.innerHTML = count++;
  5. }
  6. const setUserAction = debounce(getUserAction, 10000, true);
  7. container.onmousemove = setUserAction;
  8. document.getElementById("btn").addEventListener("click", function () {
  9. setUserAction.cancel();
  10. });

下面是每隔 10 秒执行一次

debounce3.gif

参考:

[1] javascript专题之跟着underscore学防抖
[2] 完整版例子