函数节流
基本概念
函数节流是高频率执行函数的一种优化方式,另一种方式是函数防抖,函数节流和函数防抖的区别是:
- 函数节流是在一定时间内只执行一次函数。
- 函数防抖是在函数被触发一定时间后再执行,如果在这时间段内又被触发,则重新计时。
实现
throttleFn
实现了函数节流,返回的新函数执行时就会有节流效果。
throttleFn
函数有两个参数:要执行的函数 fn
和时间段 delay
, 返回的新函数会设置一个定时器,在 delay
时间之后才会执行 fn
并清空 timeout
。
如果在定时器到期之前这个新函数再次被调用,fn
不会执行,这里定时器没有销毁,加上会更加严谨些。
优化
定时器
定时器仅仅只是计划代码在未来的某个时间执行(但是并不保证在该时间点一定执行)。执行时机是不能保证的,因为定时器只是在指定时间后将执行的函数放入执行队列中等待执行,只有当进程空闲时才会从执行队列中取出执行。
如图,添加了 200ms 的定时器,在 600ms 时会将要执行的函数放入执行队列中,这时主进程是空闲状态,就会从执行队列中取出要执行的函数执行。
如果在 250ms 时添加了 100ms 的定时器,那么 350ms 时将要执行的函数放入执行队列中,但此时主进程还在执行JavaScript代码,定时器代码最终是在 400ms 主进程空闲后才执行。
时间戳来实现函数节流
相对定时器有以下优势:
- 实时性:定时器的执行时间可能会延后执行,时间戳能更精确的控制函数的执行。
- 性能消耗:频繁的创建定时器有一定的性能消耗,时间戳的方式不用管理定时器。
- 执行方式:定时器是异步执行的,时间戳是同步执行的。
缺点:
- 执行函数如果是很耗时的操作,时间戳的同步执行反而会阻塞进程而使得页面卡顿。
- 时间戳的方式在最后一次执行的函数可能会被忽略,导致没有记录到最后一次的状态。比如在图片懒加载中用的话,就会出现问题。
图片懒加载的实现是 onscroll
事件中判断图片是否在可视区域内,onscroll
事件可以采用函数节流来进行优化,如果不记录最后一次 onscroll
事件的触发执行,就不能准确的判断图片最后是否在可视区域内。
useThrottleFn
VueUse 中使用 useThrottleFn
来实现函数节流。
使用
基本实现
首先 useThrottleFn
是采用时间戳的方式来实现函数节流。
上面提到时间戳的方式有两个问题:
第一个问题是执行函数如果是很耗时的操作的话,时间戳的同步执行返回会阻塞进程而使得页面卡顿,这个问题可以将函数的执行转为异步来解决。
第二个问题是最后一次执行的函数可能会被忽略,导致没有记录到最后一次的状态,解决这个问题可以采用定时器来设置最后一次执行。
函数加了 trailing
参数,为 true
时会执行间隔小于 delay
的加一个定时器作为最后一次执行。
异步处理
现在异步操作一般都是用 Promise
更好一些,因此对定时器做一层 Promise
封装方便获取返回值。
封装完之后,返回的新函数的返回值可能是 Promise
或者其他值,这里统一返回 Promise
更方便使用者处理。
功能拆分
我们可以将 throttleFn
函数里面的功能拆分封装。
节流的部分进行封装:
Promise
部分进行封装:
配置项扩展
添加this绑定
类似 Function.prototype.call
一样将 this 绑定的对象作为第一个参数:
使用:
首次不执行
有些场景下是希望节流的函数第一次不执行,比如在 loading
延迟赋值场景下。
loading
延迟赋值:
一般我们在发送请求获取数据的时会显示一个 loading
的效果,目的告知用户当前正在加载数据,请等待。以提升用户体验,但如果获取数据的时间很快的话,loading
的效果一闪而过反而体验不好。因此对loading
进行延迟赋值,也就是抽出一小段时间用来网络请求,超出这段时间再显示 loading
,没有超出就不显示 loading
,如果这一小段时间够短,那么用户是几乎感知不到的。
第一次 throttledFn(true)
不马上执行,而是等超过 500ms 才执行,这里就需要函数节流首次调用不马上执行。useThrottleFn
通过 leading
配置首次不执行。
ms 可能 Ref 对象或函数
完整代码:
rejectOnCancel 配置
rejectOnCancel
用于取消定时器后触发 Promise
的 reject
方法。
细节优化
将 throttleFn
改名为 useThrottleFn
,并调整参数顺序方便使用。