前言

在前端开发中会遇到一些频繁的事件触发,比如:

  1. window 的 resize、scroll
  2. mousedown、mousemove、mousewheel(鼠标滚轮)
  3. keyup(弹起键盘)、keydown(按下键盘)、keypress(按下字符键盘)
    ……

想象一下窗口的resize事件或者是一个元素的onmouseover事件 - 他们触发时,执行的非常迅速,并且触发很多次。如果你的回调过重,你可能使浏览器死掉。

这就是为什么要使用防抖。

原理

防抖的原理:在wait时间内,持续触发某个事件。第一种情况:如果某个事件触发wait秒内又触发了该事件,就应该以新的事件wait等待时间为准,wait秒后再执行此事件;第二种情况:如果某个事件触发wait秒后,未再触发该事件,则在wait秒后直接执行该事件。

通俗一点:定义wait=2000,持续点击按钮,前后点击间隔都在2秒内,则在最后一次点击按钮后,等待2秒再执行func方法。如果点击完按钮,2秒后未再次点击按钮,则2秒后直接执行func方法。

示例代码

代码一(根据原理)

定义函数debounce
根据表述,我们可以知道需要传入参数:func、wait
实现代码:

  1. function debounce(func, wait = 500) {
  2. let timeout; // 定义定时器,wait秒后需要清除定时器
  3. return function () {
  4. // 如果再次触发函数时,已有timeout,则清空销毁当前timeout,再以新的事件重新设置定时器
  5. if (timeout) clearTimeout(timeout);
  6. timeout = setTimeout(function () {
  7. func();
  8. clearTimeout(timeout)
  9. }, wait);
  10. };
  11. }

代码二(解决函数this指向)

我们之前的原函数指向哪,如果使用我们的 debounce 函数包裹后,也要将this指向正确的对象。

  1. function debounce(func, wait = 500) {
  2. let timeout, context;
  3. return function () {
  4. context = this;
  5. // 如果再次触发函数时,已有timeout,则清空销毁timeout,在往下执行
  6. if (timeout) clearTimeout(timeout);
  7. timeout = setTimeout(function () {
  8. func.apply(context);
  9. clearTimeout(timeout);
  10. }, wait);
  11. };
  12. }

代码三(解决函数event对象)

JavaScript 在事件处理函数中会提供事件对象 event;
因此,也要考虑到保持原函数的event对象相同

式一:

  1. function debounce(func, wait = 500) {
  2. let timeout, context, args;
  3. return function () {
  4. context = this;
  5. args = arguments;
  6. // 如果再次触发函数时,已有timeout,则清空销毁timeout,在往下执行
  7. if (timeout) clearTimeout(timeout);
  8. timeout = setTimeout(function () {
  9. func.apply(context, args);
  10. clearTimeout(timeout);
  11. }, wait);
  12. };
  13. }

式二:

  1. function debounce(func, wait = 500) {
  2. let timeout, context;
  3. return function (...args) {
  4. context = this;
  5. // 如果再次触发函数时,已有timeout,则清空销毁timeout,在往下执行
  6. if (timeout) clearTimeout(timeout);
  7. timeout = setTimeout(function () {
  8. func.apply(context, args);
  9. clearTimeout(timeout);
  10. }, wait);
  11. };
  12. }

代码四(函数返回值)

此时需要注意一个问题,就是我们在执行原函数时可能有返回值,我们需要处理debounce函数,在最后也要有相同返回值。

这里做出的处理,是将func.apply(context, args)单独拿出来,输出原函数的result

  1. function debounce(func, wait = 500) {
  2. let timeout, context, result;
  3. function showResult(e1, e2) {
  4. result = func.apply(e1, e2); // 绑定e1,e2的同时,输出result
  5. return result;
  6. }
  7. return function (...args) {
  8. context = this;
  9. // 如果再次触发函数时,已有timeout,则清空销毁timeout,在往下执行
  10. if (timeout) clearTimeout(timeout);
  11. // 这里是不立即执行的原代码
  12. timeout = setTimeout(function () {
  13. showResult(context, args); // 将this,arguments代入函数
  14. clearTimeout(timeout);
  15. }, wait);
  16. };
  17. }

代码五(立刻执行)

因为原理中,每次触发完后还需要等待wait秒执行。
但是某些场景,比如按钮点击后调用接口,会使整个时间变长,这时候就需要定义immediate,点击按钮,立即执行调用接口,还要达到wait秒内防抖的效果。

  1. function debounce(func, wait = 500, immediate = false) {
  2. let timeout, context, result, callNow;
  3. function showResult(e1, e2) {
  4. result = func.apply(e1, e2); // 绑定e1,e2的同时,输出result
  5. return result;
  6. }
  7. return function (...args) {
  8. context = this;
  9. // 如果再次触发函数时,已有timeout,则清空销毁timeout,在往下执行
  10. if (timeout) clearTimeout(timeout);
  11. if (immediate) {
  12. // 这里是立即执行的判断代码
  13. callNow = !timeout; // timeout最开始定义为undefined,如果未设置定时器,则!timeout返回true;否则返回false
  14. timeout = setTimeout(function () {
  15. timeout = null; // 这里时定时器走完,让timeout为null,则上一步!timeout依然返回true;
  16. }, wait);
  17. if (callNow) showResult(context, args); //刚进入timeout=undefined以及,wait时间走完timeout = null,两种情况都会立即执行函数
  18. } else {
  19. // 这里是不立即执行的原代码
  20. timeout = setTimeout(function () {
  21. showResult(context, args); // 将this,arguments代入函数
  22. clearTimeout(timeout);
  23. }, wait);
  24. }
  25. };
  26. }

代码六(取消)

增加取消防抖的方法:只需要定义cancel方法,去除定时器,将初始变量全部设置为undefined。

  1. function debounce(func, wait = 500, immediate = false) {
  2. let timeout, context, result, callNow;
  3. function showResult(e1, e2) {
  4. result = func.apply(e1, e2); // 绑定e1,e2的同时,输出result
  5. return result;
  6. }
  7. const debounced = function (...args) {
  8. context = this;
  9. // 如果再次触发函数时,已有timeout,则清空销毁timeout,在往下执行
  10. if (timeout) clearTimeout(timeout);
  11. if (immediate) {
  12. // 这里是立即执行的判断代码
  13. callNow = !timeout; // timeout最开始定义为undefined,如果未设置定时器,则!timeout返回true;否则返回false
  14. timeout = setTimeout(function () {
  15. timeout = null; // 这里时定时器走完,让timeout为null,则上一步!timeout依然返回true;
  16. }, wait);
  17. if (callNow) showResult(context, args); //刚进入timeout=undefined以及,wait时间走完timeout = null,两种情况都会立即执行函数
  18. } else {
  19. // 这里是不立即执行的原代码
  20. timeout = setTimeout(function () {
  21. showResult(context, args); // 将this,arguments代入函数
  22. clearTimeout(timeout);
  23. }, wait);
  24. }
  25. };
  26. debounced.cancel = function () {
  27. // 去除定时器,
  28. if (timeout !== undefined) {
  29. clearTimeout(timeout);
  30. }
  31. // 将初始变量全部设置为undefined
  32. timeout = context = result = callNow = undefined;
  33. };
  34. return debounced;
  35. }

演示地址

可以去Github仓库查看演示代码

跟着大佬学系列

主要是日常对每个进阶知识点的摸透,跟着大佬一起去深入了解JavaScript的语言艺术。

后续会一直更新,希望各位看官不要吝啬手中的赞。

❤️ 感谢各位的支持!!!

❤️ 如果有错误或者不严谨的地方,请务必给予指正,十分感谢!!!

❤️ 喜欢或者有所启发,欢迎 star!!!

参考