前言

js的典型的场景

  • 监听页面的scroll事件
  • 拖拽事件
  • 监听鼠标的 mousemove 事件

这些事件会频繁触发会影响性能,如果使用节流,降低频次,保留了用户体验,又提升了执行速度,节省资源。

原理

节流的原理:持续触发某事件,每隔一段时间,只执行一次。

通俗点说,3 秒内多次调用函数,但是在 3 秒间隔内只执行一次,第一次执行后 3 秒 无视后面所有的函数调用请求,也不会延长时间间隔。3 秒间隔结束后则开始执行新的函数调用请求,然后在这新的 3 秒内依旧无视后面所有的函数调用请求,以此类推。

简单来说:每隔单位时间( 3 秒),只执行一次。

实现方式

目前比较主流的实现方式有两种:时间戳、定时器。

时间戳实现

使用时间戳实现:首先初始化执行事件的时间previous为0,然后将当前的时间戳减去上次执行时间(now - previous),如果大于wait,则直接执行函数,并且将此时的执行时间now赋给previous(previous = now)。

由于首次previous = 0,则此时函数第一次触发就会立即执行。

后续则每隔wait时间执行一次,如果停止触发,则不会再执行函数。

  1. // 由于一开始now - 0 > wait,则这个写法,时间会立即执行,没过一秒会执行一次,停止触发,则不会再执行事件
  2. function throttle(func, wait = 500) {
  3. let context, now;
  4. let previous = 0; // 设置过去的执行时间初始值为0
  5. return function (...args) {
  6. context = this;
  7. now = +(Date.now() || new Date().getTime());
  8. if (now - previous > wait) {
  9. func.apply(context, args);
  10. previous = now;
  11. }
  12. };
  13. }

定时器实现

使用定时器实现:首先初始化timeout,然后定义!timeout为true的情况下,直接执行setTimeout,,等待wait时间后执行函数,然后清空timeout,以此类推,重新进入也会按上述执行。

由于进入函数,就执行setTimeout,所以不会立即触发函数执行。

后续则每隔wait时间执行一次,如果停止触发,而后还会触发执行一次函数。

  1. // 由于一进入就创建了定时器,所以不会立即触发函数执行
  2. function throttle(func, wait = 500) {
  3. let context, timeout;
  4. return function (...args) {
  5. context = this;
  6. if (!timeout) {
  7. timeout = setTimeout(function () {
  8. timeout = null;
  9. func.apply(context, args);
  10. }, wait);
  11. }
  12. };
  13. }

合并版本

如果,我们需要既刚开始就立即执行,停止触发后,还会触发执行一次函数。

下面,我们将定时器和时间戳合并,组成一个全新的节流版本。

  1. function throttle(func, wait = 500) {
  2. let context, timeout, result;
  3. let previous = 0;
  4. const throttled = function (...args) {
  5. context = this;
  6. const now = +(Date.now() || new Date().getTime()); // 当前时间
  7. // 下次触发 func 剩余时间
  8. const remaining = wait - (now - previous);
  9. // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行
  10. if (remaining <= 0 || remaining > wait) {
  11. if (timeout) {
  12. clearTimeout(timeout);
  13. timeout = null;
  14. }
  15. previous = now;
  16. func.apply(context, args);
  17. } else if (!timeout) {
  18. // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次
  19. timeout = setTimeout(function () {
  20. timeout = null;
  21. previous = +(Date.now() || new Date().getTime()); // 这里是将previous重新赋值当前时间
  22. func.apply(context, args);
  23. }, remaining);
  24. }
  25. };
  26. return throttled;
  27. }

合并版本优化

由于合并后的版本并没用返回值的优化+取消功能。

下面对代码进行返回值+取消功能优化:

  1. function throttle(func, wait = 500) {
  2. let context, timeout, result;
  3. let previous = 0;
  4. const showResult = function (e1, e2) {
  5. result = func.apply(e1, e2);
  6. return result;
  7. };
  8. const throttled = function (...args) {
  9. context = this;
  10. const now = +(Date.now() || new Date().getTime()); // 当前时间
  11. // 下次触发 func 剩余时间
  12. const remaining = wait - (now - previous);
  13. // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行
  14. if (remaining <= 0 || remaining > wait) {
  15. if (timeout) {
  16. clearTimeout(timeout);
  17. timeout = null;
  18. }
  19. previous = now;
  20. showResult(context, args);
  21. } else if (!timeout) {
  22. // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次
  23. timeout = setTimeout(function () {
  24. timeout = null;
  25. previous = +(Date.now() || new Date().getTime()); // 这里是将previous重新赋值当前时间
  26. showResult(context, args);
  27. }, remaining);
  28. }
  29. };
  30. throttled.cancel = function () {
  31. if (timeout !== undefined) {
  32. clearTimeout(timeout);
  33. }
  34. previous = 0;
  35. context = timeout = result = undefined;
  36. };
  37. return throttled;
  38. }

功能性优化

有时候,我们也希望无头有尾,或者有头无尾。

  1. function throttle(func, wait = 500, options = {}) {
  2. let context, timeout, result;
  3. let previous = 0;
  4. // 如果同时设置无头无尾,则直接使用默认设置,其他情况,则走下述操作
  5. if (!(options.leading === false && options.trailing === false)) {
  6. leading = !!options.leading; // 默认去除立即执行部分
  7. trailing = "trailing" in options ? !!options.trailing : true; // 默认保留尾部
  8. }
  9. // 返回原函数的return
  10. const showResult = function (e1, e2) {
  11. result = func.apply(e1, e2);
  12. return result;
  13. };
  14. // 获取当前时间
  15. const getNow = function () {
  16. return +(Date.now() || new Date().getTime());
  17. };
  18. const throttled = function (...args) {
  19. context = this;
  20. const now = getNow(); // 当前时间
  21. // 下次触发 func 剩余时间
  22. if (!previous && leading === false) previous = now;
  23. const remaining = wait - (now - previous);
  24. // 如果没有剩余时间或者改了系统时间,这时候不需要等待,直接立即执行,这样就会第一次就执行
  25. if (remaining <= 0 || remaining > wait) {
  26. if (timeout) {
  27. clearTimeout(timeout);
  28. timeout = null;
  29. }
  30. previous = now;
  31. showResult(context, args);
  32. } else if (!timeout && trailing !== false) {
  33. // 剩余的情况就是remaining<=wait的情况,这里使用setTimeout就可以最后也会执行一次
  34. timeout = setTimeout(function () {
  35. timeout = null;
  36. previous = options.leading === false ? 0 : getNow(); // 这里是将previous重新赋值当前时间
  37. showResult(context, args);
  38. }, remaining);
  39. }
  40. };
  41. throttled.cancel = function () {
  42. if (timeout !== undefined) {
  43. clearTimeout(timeout);
  44. }
  45. previous = 0;
  46. context = timeout = result = undefined;
  47. };
  48. return throttled;
  49. }

这里,如果options不传参数,函数默认设置

  1. let leading = false
  2. let trailing = true

也就是无头有尾。

如果同时设置无头无尾,则会直接采用默认设置,无头有尾。

  1. // 如果同时设置无头无尾,则直接使用默认设置,其他情况,则走下述操作
  2. if (!(options.leading === false && options.trailing === false)) {
  3. leading = !!options.leading; // 默认去除立即执行部分
  4. trailing = "trailing" in options ? !!options.trailing : true; // 默认保留尾部
  5. }

演示地址

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

跟着大佬学系列

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

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

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

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

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

参考