throttle(节流)和 debounce(防抖)
前言
今天看到京东 Taro 一个页面,感觉进度条怪怪的(请缩小屏幕宽度或者使用手机端),总有一种跟不上的感觉,一看代码才知道,transition
设置成 50ms,看桌面端的呢,0.6s,效果就舒服很多了~然后想到这个可以写个文章……………
需求
像京东这个进度条,实现原理就是通过scroll
事件来驱动的,但是如果你直接用scroll
事件,就会发现触发频率非常之高,如果每次都通过这样的方式然后计算高度到底看到哪了,十分小号性能,而且这是没必要的,所以诞生出 throttle(节流),意思就是,一个单位时间内,只会触发一次,比如滑到底部需要一秒,那么我们算 24 帧去检查,就可以做到同样效果而且有效避免无谓性能消耗。
throttle
先上代码,这边的核心代码是throttle
,实现起来也很简单
1 | throttle = function(func, delay) { |
通过闭包实现一个函数的包装, 实现内部timer
的存在,每次触发之后计时器运行,执行之后重置为undefined
debounce
标题写了防抖,那就肯定还有一个东西,其实也和throttle
类似,只不过他不是单位时间内只允许一次,而是,如果你在单位时间内重复了操作,那么这个时间重置,形象一点的 🌰 就是用户输入数据检查,比如手机号,其实没输完之前去检查他是没意义的,那么可以等到用户停止输入的一刻,再去检查,在他输入的过程中不断重置时间,我这边懒得写第二个,就用同样的进度条 🌰 看看
核心代码
1 | debounce = function(func, delay) { |
逻辑没啥好说,基本差不多,做全一点可以给返回的方法里面加一个cancel
来取消单次执行。
定时器是怎么运作
其实这些写起来都不难,但是定时器是如何运作的呢?JavaScript 是单线程如何实现定时器功能?浏览器和 Node.JavaScript 环境是否又不一样呢?
浏览器
一般来讲,浏览器会多个常驻线程:
- GUI 渲染线程
- 主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等。
- 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
- 该线程与 JavaScript 引擎线程互斥,当执行 JavaScript 引擎线程时,GUI 渲染会被挂起,当任务队列空闲时,主线程才会去执行 GUI 渲染。
- JavaScript 引擎线程
- 该线程当然是主要负责处理 JavaScript 脚本,执行代码。
- 也是主要负责执行准备好待执行的事件,将依次进入任务队列,等待 JavaScript 引擎线程的执行。
- 当然,该线程与 GUI 渲染线程互斥,当 JavaScript 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞。
- 定时触发器线程
- 负责执行异步定时器一类的函数的线程,如:
setTimeout
,setInterval
。 - 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 JavaScript 引擎线程执行。
- 负责执行异步定时器一类的函数的线程,如:
- 事件触发线程
- 主要负责将准备好的事件交给 JavaScript 引擎线程执行,比如定时器,网页的事件,请求回调等等
- 异步 http 请求线程
- 负责执行异步请求一类的函数的线程,如: Promise,axios,ajax 等。
- 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JavaScript 引擎线程执行。
其实浏览器就是通过定时器触发器线程来处理,底层可能就是调用 Native 层去实现,然后计时结束,就会加入到事件触发线程,然后事件触发线程会加入到 JavaScript 引擎线程,由于要经过这个排队时间,所以这个定时,并不准确。
Node.js
Node.js 我们知道,底层是 libuv,而底层 libuv 里面有六个事件循环,不断运行:
- timers 阶段:这个阶段执行 timer(
setTimeout
、setInterval
)的回调 - pending callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
- idle, prepare 阶段:仅 node 内部使用
- poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
- check 阶段:执行
setImmediate
的回调 - close callbacks 阶段:一些关闭回调,例如
socket.on('close',...)
这边可以看到也是有一个专门的 timer 阶段,也是交给底层实现,然后回调。
clearTimeout
由于定时器都是底层实现,所以如果我们需要清除定时器的话,就需要用到系统 APIclearTimeout
,如果你单纯的重置 timer 也没有任何效果,毕竟不能自欺欺人…底层已经在计时了。
总结
throttle 和 debounce 十分常用,毕竟前端就是跟用户打交道,但是里面实现核心 timer 可以牵涉十分多内容,event loop 运作,浏览器细看还能发现回流等字眼,发现小细节,才能更理解代码到底是怎么运作的。