《模拟undescore学防抖中》,我们了解到为啥要限制频繁操作,但是除了防抖可以限制以外还有没有别的办法呢?
答案是肯定有的,所以今天我们就来讲讲另一种限制频繁操作的方式:节流

节流的原理

如果你持续触发某个事件,每隔一段时间,只执行一次这个事件。

根据首次是否执行以及停止出发后是否再执行一次,效果有所不同,实现的方式也有所不同。
我们用leading代表首次是否执行,trailing代表停止触发后是否再执行一次

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

使用时间戳

首先来看一个简单的示例,高频鼠标移动事件:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>跟着undescore学节流</title>
  8. <style>
  9. #container {
  10. width: 400px;
  11. height: 200px;
  12. line-height: 200px;
  13. margin: 0 auto;
  14. margin-top: 200px;
  15. background-color: rgb(219, 68, 68);
  16. color: #FFF;
  17. font-size: 16px;
  18. text-align: center;
  19. }
  20. .cancel {
  21. display: block;
  22. margin: 0 auto;
  23. }
  24. </style>
  25. </head>
  26. <body>
  27. <div id="container">
  28. </div>
  29. <script type="text/javascript">
  30. var box = document.querySelector('#container');
  31. var count = 0;
  32. function getUserAction() {
  33. box.innerHTML = count++;
  34. }
  35. box.addEventListener('mousemove', getUserAction);
  36. </script>
  37. </body>
  38. </html>

效果如下:
节流gif1.gif

以上示例中,一秒钟这个事件触发了上百次,我们如何来进行优化呢?
来看看第一种方法: 使用时间戳,当触发事件的时候我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为0),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于就不执行。

看了这个表述,是不是感觉已经可以写出代码了……让我们来写第一版的代码:

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

在我们的例子中使用这个函数:

  1. box.addEventListener('mousemove', throttle(getUserAction, 1000));

效果演示如下:
节流gif2.gif

我们可以看到:当鼠标移入时,事件立即执行,没过1s会执行一次,如果在6~7s之间停止触发,以后不会再执行事件。

使用定时器

接下来讲讲第二种实现方式,使用定时器。

当触发事件的时候,我们设置一个定时器,定时器延时执行之前不会触发事件,在定时器中执行函数并清除定时器,这样就可以设置下一个定时器。所以我们来实现下第二版代码:

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

将第二版代码加入之前的例子中,我们来看下效果:
节流gif3.gif

我设置的事件间隔为1s。当我们鼠标移入的时候,事件不会立即执行,间隔1s以后执行事件,此后只要触发此事件,事件就会以1s的间隔执行,一旦停止触发,最后1s内会执行一次。然后停止执行。

所以我们比较下两个方法:
1、时间戳方法:事件会立即执行;定时器方法:事件会在n秒后第一次执行
2、时间戳方法:停止触发后没有办法再执行事件;定时器方法:事件停止触发后依然会再执行一次事件。

合并优化方法

假如存在一个这样的需求:我想要一个事件一触发就立即执行,停止触发后还能执行一次的节流函数!

那针对这个需求我们怎么实现呢?当事件触发的时候立即执行,那我们就需要借助时间戳的方法,在设置时间间隔内立即执行第一次事件函数,再进行判断是否含有定时器,如果有就在设置的时间间隔后执行。

根据以上原理我们来简单实现一下吧:

  1. // 第三版
  2. function throttle(func, wait) {
  3. var timeout, args, context, result;
  4. var previous = 0;
  5. var later = function() {
  6. previous = +new Date();
  7. timeout = null;
  8. func.apply(context, args);
  9. }
  10. var throttled = function() {
  11. var now = +new Date();
  12. // 下次触发func剩余的时间
  13. var remaining = wait - (now - previous);
  14. context = this;
  15. args = arguments;
  16. // 如果没有剩余时间了或者你修改了系统时间
  17. if (remaining <= 0 || remaining > wait) {
  18. if (timeout) {
  19. clearTimeout(timeout);
  20. timeout = null;
  21. }
  22. previous = now;
  23. func.apply(context, args);
  24. } else if (!timeout) {
  25. timeout = setTimeout(later, remaining);
  26. }
  27. }
  28. return throttled;
  29. }

为了让展示效果明显一点,我们将时间间隔改成3s,假如示例中

  1. box.addEventListener('mouseomove', throttle(getUserAction, 3000));

效果如下:

节流gif4.gif

我们可以看到:当鼠标移入时,事件立即执行了,鼠标移动了3s后,事件再一次触发。当数字变成2的时候,我们停止触发事件,9s的时候依旧会执行一次事件。但是假如我们需要执行一个立即执行,但是停止触发后后不会再执行一次或者不会立即执行,停止触发后仍然会执行一次。这个该咋办呢?

不要慌,我们把第三版代码进行一个封装,给它一个参数options,根据传的值判断到底是哪种效果。options中约定:
leading:false 表示禁用第一次执行
trailing:false 表示禁用停止触发的回调

我们来改下代码:

  1. // 第四版
  2. function throttle(func, wait, options) {
  3. var timeout, context, args, result;
  4. var previous = 0;
  5. if (!options) options = {};
  6. var later = function() {
  7. previous = options.leading === false ? 0 : new Date().getTime();
  8. timeout = null;
  9. func.apply(context, args);
  10. if (!timeout) context = args = null;
  11. };
  12. var throttled = function() {
  13. var now = new Date().getTime();
  14. if (!previous && options.leading === false) previous = now;
  15. var remaining = wait - (now - previous);
  16. context = this;
  17. args = arguments;
  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. if (!timeout) context = args = null;
  26. } else if (!timeout && options.trailing !== false) {
  27. timeout = setTimeout(later, remaining);
  28. }
  29. };
  30. return throttled;
  31. }

需要注意的是,leading:false和trailing:false不能同时设置,如果同时设置的话,比如当你鼠标移出的时候,因为trailing设置为false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立即执行。就违反了leading:false,bug就出来了,所以,这个throttle只有三种用法:

  1. // 有头有尾
  2. box.addEventListener('mousemove', throttle(getUserAction, 3000));
  3. // 无头有尾
  4. box.addEventListener('mousemove', throttle(getUserAction, 3000, { leading: false }));
  5. // 有头无尾
  6. box.addEventListener('mousemove', throttle(getUserAction, 3000, { trailing: false }));

其中第二、三种展示效果如下:

节流gif5.gif

节流gif6.gif

取消

当然了,在防抖的模拟实现中,我们加了一个cancel方法,所以我们在节流中也可以加个cancel方法:

  1. // 第五版
  2. function throttle(func, wait, options) {
  3. var timeout, context, args, result;
  4. var previous = 0;
  5. if (!options) options = {};
  6. var later = function() {
  7. previous = options.leading === false ? 0 : new Date().getTime();
  8. timeout = null;
  9. func.apply(context, args);
  10. if (!timeout) context = args = null;
  11. };
  12. var throttled = function() {
  13. var now = new Date().getTime();
  14. if (!previous && options.leading === false) previous = now;
  15. var remaining = wait - (now - previous);
  16. context = this;
  17. args = arguments;
  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. if (!timeout) context = args = null;
  26. } else if (!timeout && options.trailing !== false) {
  27. timeout = setTimeout(later, remaining);
  28. }
  29. };
  30. throttle.cancel = function() {
  31. clearTimeout(timeout);
  32. previous = 0;
  33. timeout = null;
  34. }
  35. return throttled;
  36. }

在示例中的使用方法:

  1. var button = document.querySelector('.cancel');
  2. var box = document.querySelector('#container');
  3. var setUserAction = throttle(getUserAction, 3000);
  4. box.addEventListener('mousemove', setUserAction);
  5. button.addEventListener('click', function() {
  6. setUserAction.cancel()
  7. })

示例效果:

节流gif7.gif
到这里模拟underscore的节流函数就算是完成了🎉🎉🎉


:::info 本文档内容参考:冴羽大大的博客 :::