Words etch cherished moments.
OG

手写函数节流

3,282 words, 8 min read
2019/03/03 AM
190 views

#手写函数节流(throttle)

在前端开发中,我们经常遇到需要对某些频繁触发的事件(如窗口的resize、scroll事件,或者高频率的按钮点击)进行限制,以避免因函数执行过于频繁而导致的性能问题。这时,一个非常有用的工具就是函数节流(throttle)。本文将深入探讨函数节流的概念、原理,并手把手教你如何实现一个简单的函数节流。

#什么是函数节流?

函数节流是指一定时间内只执行一次函数,如果函数被频繁触发,则只有一次触发生效。即使该函数被连续调用多次,也只会按照设定的时间间隔执行一次,这样可以有效减少函数执行的频率,避免不必要的计算和渲染,从而提升应用的性能。

#为什么需要函数节流?

想象一下,用户在快速滚动页面时,如果为scroll事件绑定了复杂的处理逻辑(如加载更多数据、计算滚动位置等),每次滚动都会触发这个逻辑,这将极大地消耗系统资源,导致页面卡顿。通过引入函数节流,我们可以确保这些操作仅在用户停止滚动或滚动到一定间隔后才执行,从而提高用户体验。

#函数节流的实现

函数节流的实现有不同的思路,可以通过时间戳实现,也可以通过定时器实现。

#时间戳

只要触发,就用Date获取现在的时间,与上一次的时间进行比较。如果时间差大于了规定的等待时间,就执行一次;目标函数执行以后,就更新 previous 值,确保它是“上一次”的时间,否则就等下一次触发时继续比较。

          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
function throttle(func, delay) { let previous = 0; return function() { const now = Date.now(); const context = this; if (now - previous >= delay) { func.apply(context, arguments) // 执行后更新 previous 值 previous = now; } }; }

#定时器

用定时器实现间隔,当定时器不存在时,说明可以执行函数,于是定义一个定时器来向任务队列注册目标函数,目标函数执行后设置保存定时器变量为空,当定时器已经被定义,说明已经在等待过程中,则等待下次触发时间时在进行查看。

          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
function throttle(func, delay) { let timer, context; return function() { context = this; if (!timer) { timer = setTimeout(function() { func.apply(contenxt, arguments); timer = null; }, delay) } } }

#两者差异

时间戳:先执行目标函数,后等待规定的时间; 计时器:先等待规定时间,再执行。停止触发事件后,若定时器已经在任务队列里注册了目标函数,它也会执行最后一次。

#优化

结合二者,实现一次触发,两次执行(先立即执行,结尾也有执行)

          
  • 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
function throttle(func, delay) { let previous = 0; let context, args, timer; return function() { let now = new Date(); context = this; args = arguments; if (now - previous >= delay) { // 当距上一次执行的间隔大于规定时间,直接执行 func.apply(context, args); previous = now; } else { // 否则继续等待,结尾执行一次 if (timer) { clearTimeout(tiemr); } timer = setTimeout(() => { func.apply(context, args); timer = null; }, delay) } } }

已经实现了一次触发,两次执行,有头有尾的效果

#问题

上一个周期的“尾”和下一个周期的“头”之间,失去了对时间间隔的控制。

在通过计时器注册入任务队列后执行的情况下,忽略了 previous 的更新。 导致了 previous 的值不再是“上一次执行”时的时间,而是“上一次直接可执行情况下执行”的时间。 同时,引入变量 remaining 表示还需要等待的时间,来让尾部那一次的执行也符合时间间隔。

          
  • 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
function throttle(func, delay) { let previous = 0; let context, args, timer, remaining; return function() { let now = new Date(); context = this; args = arguments; // 剩余的还需要等待的时间 remaining = delay - (now - previous); if (remaining <= 0) { // 当距上一次执行的间隔大于规定时间,直接执行 func.apply(context, args); // 重置“上一次执行”的时间 previous = now } else { if (timer) { clearTimeout(tiemr); } timer = setTimeout(() => { func.apply(context, args); timer = null; // 重置“上一次执行”的时间 previous = new Date(); //等待还需等待的时间 }, remaining) } } }
Creative Commons BY-NC 4.0
https://zhangwurui.cn/article/41
0/0comments
Guest
Start the discussion...
Be the first to comment