在上一遍笔记里已经实现了防抖函数,这次要实现一下节流函数。节流函数也是限制事件的频繁触发。

节流的原理:

如果持续触发事件,每隔一段时间,只执行一次。根据首次是否执行以及结束是否执行,效果有所不同,实现方式也有所不同。

有两种主流的实现方式:使用时间戳,使用定时器。

时间戳

当触发事件的时候,取出当前的时间戳,然后减去之前的时间戳(一开始设置值为 0),如果大于设置的时间周期(wait),就执行函数,然后更新时间戳为当前时间的时间戳,如果小于,就不执行。

  1. // 第一版
  2. function throttle(func, wait) {
  3. let context, args, previous = 0;
  4. return function () {
  5. let now = +new Date();
  6. context = this;
  7. args = arguments;
  8. if (now - previous > wait) {
  9. func.apply(context, args);
  10. previous = now
  11. }
  12. };
  13. }

还是以防抖函数里面的例子为例

throttle.gif

当鼠标初次移入时,会立即触发执行,然后每过一秒会执行一次,如果在 4.3s 停止触发,以后不会再执行事件。

定时器

当触发事件的时候,设置一个定时器,在触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清理定时器,这样就可以设置下一个定时器。

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

throttle1.gif

当鼠标初次移入的时候,事件不会立即执行,晃了 2s 后终于执行了一次,之后每隔 2s 执行一次,当数字显示为 3 时,立即移出鼠标,相当于在 6.2s 的时候停止触发,但是在第 8s 的时候执行一次事件。

合二为一

那能不能写一个有头有尾的呢?既鼠标初次移入时立即执行一次,停止触发的时候还能再执行一次。综合时间戳和定时器的优势。

  1. // 第三版
  2. function throttle(func, wait) {
  3. let timeout, context, args;
  4. let previous = 0;
  5. const later = function () {
  6. previous = +new Date();
  7. timeout = null;
  8. func.apply(context, args);
  9. };
  10. return function () {
  11. const now = +new Date();
  12. //下次触发 func 剩余的时间
  13. const remaining = wait - (now - previous);
  14. context = this;
  15. args = arguments;
  16. // 如果没有剩余的时间了或者你改了系统时间
  17. // 首次触发或者触发的时候距离上一次的触发时间大于 wait 时间
  18. if (remaining <= 0 || remaining > wait) {
  19. if (timeout) {
  20. clearTimeout(timeout);
  21. timeout = null;
  22. }
  23. previous = now;
  24. func.apply(context, args);
  25. // 距离上次触发的时间小于 wait 时间
  26. // 不管触发几次还是以第一次为主
  27. } else if (!timeout) {
  28. timeout = setTimeout(later, remaining);
  29. }
  30. };
  31. }

throttle2.gif

可以看到当鼠标初次移入的时候,会立即执行一次,之后每隔 2s 执行一次,当数字是 2 的时候立即移出鼠标,也就时大概在 4.2s 的时候停止触发事件,但是在 6s 的时候也会执行一次触发。

优化

能不能在首次触发结尾不触发,或者首次不触发结尾触发了?

这是需要一个 options 作为第三个参数,它有两个值

leading:false 表示禁用第一次执行。
trailing:false 表示禁用停止触发的回调。

  1. // 第四版
  2. function throttle(func, wait, options) {
  3. let timeout, context, args;
  4. let previous = 0;
  5. const later = function () {
  6. previous = options.leading === false ? 0 : +new Date();
  7. timeout = null;
  8. func.apply(context, args);
  9. context = args = null;
  10. };
  11. const throttled = function () {
  12. const now = +new Date();
  13. if (!previous && options.leading === false) previous = now;
  14. //下次触发 func 剩余的时间
  15. const remaining = wait - (now - previous);
  16. context = this;
  17. args = arguments;
  18. // 如果没有剩余的时间了或者你改了系统时间
  19. // 首次触发或者触发的时候距离上一次的触发时间大于 wait 时间
  20. if (remaining <= 0 || remaining > wait) {
  21. if (timeout) {
  22. clearTimeout(timeout);
  23. timeout = null;
  24. }
  25. previous = now;
  26. func.apply(context, args);
  27. if (!timeout) context = args = null;
  28. // 距离上次触发的时间小于 wait 时间
  29. // 不管触发几次还是以第一次为主
  30. } else if (!timeout && options.trailing !== false) {
  31. timeout = setTimeout(later, remaining);
  32. }
  33. };
  34. return throttled;
  35. }

取消

在 throttle 函数内部添加以下代码

  1. throttled.cancel = function () {
  2. clearTimeout(timeout);
  3. previous = 0;
  4. timeout = null;
  5. };

注意

leading: falsetrailing: false 不能同时设置,如果同时设置的话,当鼠标初次移入时不会触发执行,停止触发时不会设置 later,但只要过了 wait 规定的时间,再次移入的话,就会立即执行,违反了 leading: false

所以只有三种写法

  1. container.onmousemove = throttle(getUserAction, 1000);
  2. container.onmousemove = throttle(getUserAction, 1000, {
  3. leading: false
  4. });
  5. container.onmousemove = throttle(getUserAction, 1000, {
  6. trailing: false
  7. });

参考:

[1] JavaScript专题之跟着 underscore 学节流
[2] 完整版例子