前言
js 的典型的场景
- 监听页面的 scroll 事件
 
- 拖拽事件
 
- 监听鼠标的 mousemove 事件
… 
这些事件会频繁触发会影响性能,如果使用节流,降低频次,保留了用户体验,又提升了执行速度,节省资源。
原理
节流的原理:持续触发某事件,每隔一段时间,只执行一次。
通俗点说,3 秒内多次调用函数,但是在 3 秒间隔内只执行一次,第一次执行后 3 秒 无视后面所有的函数调用请求,也不会延长时间间隔。3 秒间隔结束后则开始执行新的函数调用请求,然后在这新的 3 秒内依旧无视后面所有的函数调用请求,以此类推。
简单来说:每隔单位时间( 3 秒),只执行一次。
实现方式
目前比较主流的实现方式有两种:时间戳、定时器。
时间戳实现
使用时间戳实现:首先初始化执行事件的时间 previous 为 0,然后将当前的时间戳减去上次执行时间(now - previous),如果大于 wait,则直接执行函数,并且将此时的执行时间 now 赋给 previous(previous = now)。
由于首次 previous = 0,则此时函数第一次触发就会立即执行。
后续则每隔 wait 时间执行一次,如果停止触发,则不会再执行函数。
1 2 3 4 5 6 7 8 9 10 11 12 13
   | // 由于一开始now - 0 > wait,则这个写法,时间会立即执行,没过一秒会执行一次,停止触发,则不会再执行事件 function throttle(func, wait = 500) {     let context, now;     let previous = 0; // 设置过去的执行时间初始值为0     return function (...args) {         context = this;         now = +(Date.now() || new Date().getTime());         if (now - previous > wait) {             func.apply(context, args);             previous = now;         }     }; }
   | 
 
定时器实现
使用定时器实现:首先初始化 timeout,然后定义!timeout 为 true 的情况下,直接执行 setTimeout,,等待 wait 时间后执行函数,然后清空 timeout,以此类推,重新进入也会按上述执行。
由于进入函数,就执行 setTimeout,所以不会立即触发函数执行。
后续则每隔 wait 时间执行一次,如果停止触发,而后还会触发执行一次函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | // 由于一进入就创建了定时器,所以不会立即触发函数执行 function throttle(func, wait = 500) {     let context, timeout;
      return function (...args) {         context = this;
          if (!timeout) {             timeout = setTimeout(function () {                 timeout = null;                 func.apply(context, args);             }, wait);         }     }; }
   | 
 
合并版本
如果,我们需要既刚开始就立即执行,停止触发后,还会触发执行一次函数。
下面,我们将定时器和时间戳合并,组成一个全新的节流版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
   | function throttle(func, wait = 500) {     let context, timeout, result;     let previous = 0;     const throttled = function (...args) {         context = this;         const now = +(Date.now() || new Date().getTime()); // 当前时间         // 下次触发 func 剩余时间         const remaining = wait - (now - previous);
          // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行         if (remaining <= 0 || remaining > wait) {             if (timeout) {                 clearTimeout(timeout);                 timeout = null;             }             previous = now;             func.apply(context, args);         } else if (!timeout) {             // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次             timeout = setTimeout(function () {                 timeout = null;                 previous = +(Date.now() || new Date().getTime()); // 这里是将previous重新赋值当前时间                 func.apply(context, args);             }, remaining);         }     };     return throttled; }
  | 
 
合并版本优化
由于合并后的版本并没用返回值的优化+取消功能。
下面对代码进行返回值+取消功能优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
   | function throttle(func, wait = 500) {     let context, timeout, result;     let previous = 0;
      const showResult = function (e1, e2) {         result = func.apply(e1, e2);         return result;     };
      const throttled = function (...args) {         context = this;         const now = +(Date.now() || new Date().getTime()); // 当前时间         // 下次触发 func 剩余时间         const remaining = wait - (now - previous);
          // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行         if (remaining <= 0 || remaining > wait) {             if (timeout) {                 clearTimeout(timeout);                 timeout = null;             }             previous = now;             return showResult(context, args);         } else if (!timeout) {             // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次             timeout = setTimeout(function () {                 timeout = null;                 previous = +(Date.now() || new Date().getTime()); // 这里是将previous重新赋值当前时间                 return showResult(context, args);             }, remaining);         }         retrun result     };
      throttled.cancel = function () {         if (timeout !== undefined) {             clearTimeout(timeout);         }         previous = 0;         context = timeout = result = undefined;     };     return throttled; }
  | 
 
功能性优化
有时候,我们也希望无头有尾,或者有头无尾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
   | function throttle(func, wait = 500, options = {}) {     let context, timeout, result;     let previous = 0;
     // 如果同时设置无头无尾,则直接使用默认设置,其他情况,则走下述操作     if (!(options.leading === false && options.trailing === false)) {         leading = !!options.leading; // 默认去除立即执行部分         trailing = "trailing" in options ? !!options.trailing : true; // 默认保留尾部     }
      // 返回原函数的return     const showResult = function (e1, e2) {         result = func.apply(e1, e2);         return result;     };
      // 获取当前时间     const getNow = function () {         return +(Date.now() || new Date().getTime());     };
      const throttled = function (...args) {         context = this;         const now = getNow(); // 当前时间         // 下次触发 func 剩余时间         if (!previous && leading === false) previous = now;         const remaining = wait - (now - previous);
          // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行         if (remaining <= 0 || remaining > wait) {             if (timeout) {                 clearTimeout(timeout);                 timeout = null;             }             previous = now;             return showResult(context, args);         } else if (!timeout && trailing !== false) {             // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次             timeout = setTimeout(function () {                 timeout = null;                 previous = options.leading === false ? 0 : getNow(); // 这里是将previous重新赋值当前时间                 return showResult(context, args);             }, remaining);         }         return result;     };
      throttled.cancel = function () {         if (timeout !== undefined) {             clearTimeout(timeout);         }         previous = 0;         context = timeout = result = undefined;     };     return throttled; }
  | 
 
这里,如果 options 不传参数,函数默认设置
1 2
   | let leading = false let trailing = true
   | 
 
也就是无头有尾。
如果同时设置无头无尾,则会直接采用默认设置,无头有尾。
1 2 3 4 5
   | // 如果同时设置无头无尾,则直接使用默认设置,其他情况,则走下述操作 if (!(options.leading === false && options.trailing === false)) {     leading = !!options.leading; // 默认去除立即执行部分     trailing = "trailing" in options ? !!options.trailing : true; // 默认保留尾部 }
   | 
 
演示地址
可以去Github 仓库查看演示代码
跟着大佬学系列
主要是日常对每个进阶知识点的摸透,跟着大佬一起去深入了解 JavaScript 的语言艺术。
后续会一直更新,希望各位看官不要吝啬手中的赞。
❤️ 感谢各位的支持!!!
❤️ 如果有错误或者不严谨的地方,请务必给予指正,十分感谢!!!
❤️ 喜欢或者有所启发,欢迎 star!!!
参考
原文地址
【跟着大佬学 JavaScript】之节流