在网上也看了不少关于javascript事件循环的文章,多数是以浏览器事件循环与nodejs中事件循环做对比,分析两种环境的差异。下面说的内容是浏览器事件循环与前端性能之间的关系,了解之后在开发中规避一些性能问题。以下所探讨的事件循环皆指浏览器的事件循环,本文属于个人理解,如有不对欢迎提点。
1 事件循环
提到事件循环,相信现在多数前端小白并不陌生了。首先事件循环分为宏任务和微任务。
宏任务:鼠标事件,键盘事件,网络事件,Html解析, 定时器等;
微任务:Dom变化,Promise;
当这些事件发生的时候浏览器会在对应的事件队列(宏任务队列和微任务队列)中添加该事件处理器(回调函数)等待执行。而事件循环是一直循环着看事件队列中是否还有未处理的处理器,如果有的话就从队列首位取出处理器执行,执行完之后就移除对应处理器,就是这样一直循环着直到浏览器关闭为止,即使事件队列为空!但是在处理宏任务队列和微任务队列的方式不同。
在一次事件循环中只能处理一个宏任务,而需要处理所有微任务直到微任务队列清空
说明:事件循环是在主程序执行完之后就开始循环执行事件,主程序说简单点就是一个js文件从第一行执行到最后一行,主程序结束(此处说的是该应用就一个js文件)
举一个生活中的例子:
银行中排队办理业务的时候,假设每个排队等待办理的普通客户是宏任务,而VIP客户是微任务。那么在银行早上开始上班的之前,柜员启动相应的机器(电脑,打印机等)属于主程序,等一切准备好之后就开始办理业务了。假如现在有10个普通用户(宏任务)在等待,开始处理第一个客户,处理完之后叫号第二个客户,开始处理。如果在处理第二个客户的时候突然来个一个vip用户,那么在处理为完普通用户之后就轮到处理VIP用户(微任务),如果在处理VIP用户的时候又来了个VIP用户的话3号普通客户就得等待处理完两个VIP客户。当然此处举例皆是只该银行就一个服务窗口,复合js单线程模式。
下图是一个完整的事件循环图:
1536470185534.jpg
2 浏览器渲染UI渲染
浏览器通常会尝试60秒渲染一次页面,以达到每秒60帧(60fps)的速度, 60fps是检验前端体验是否流畅的标准,在动画里就意味着当浏览器没16ms一次刷新的话,体验是最流畅的,所有在整个页面生命周期中每一次事件循环都应该在16ms内完成。
但是在事件循环中可能会处理比较耗时的任务,那么渲染就会延后这样就会导致动画看上去不流畅,更严重的可能是页面处于假死状态等影响用户体验。
如果加上UI渲染的话上面的事件循环图是这样的
1536470913687.jpg
前端性能
上面提到浏览器会尝试16ms重新渲染页面,那么假如在处理事件的时候遇到耗时任务的时候,浏览器将会延迟渲染页面,如果处理不当那将会导致体验差问题。接下来仔细分析以下整个流程。
首先先排除为任务,只有宏任务的程序。现在有两个点击事件
<button id="bt1">btn1</button>
<button id="bt1">btn1</button>
<script>
const btn1 = document.querySelector('#btn1')
const btn2 = document.querySelector('#btn2')
btn1.addEventListener('click', () => {
console.log('运行需要8ms')
})
btn2.addEventListener('click', () => {
console.log('运行需要10ms')
})
</script>
假如主线程运行需要15ms,btn1事件处理器完成需要8ms, btn2需要10ms;那么整个流程是这样:
1.jpg
假如用户点击速度很快的时候,主线程运行期间在5ms和12ms时候分别点击了btn1,btn2按钮,触发点击事件,此时因为还处与主线程运行时间内,所有此时会向宏任务队列添加两个事件任务。当主线程运行到15ms时结束,此时浏览器可以重新渲染。渲染完之后进入事件循环,首先从任务队列中取出任务btn1事件处理器执行,此过程会耗时8ms,处理完之后结束一次事件循环,并移除事件处理器btn1。此时浏览器可以重新渲染,渲染完之后又进行到第二次事件循环,重复上述步骤,直到事件队列清空为止。
如果事件循环中存在为任务的话流程如下:
<button id="bt1">btn1</button>
<button id="bt1">btn1</button>
<script>
const btn1 = document.querySelector('#btn1')
const btn2 = document.querySelector('#btn2')
btn1.addEventListener('click', () => {
// promise1
promise.resolve().then( _ => {
console.log('运行需要4ms')
})
})
btn2.addEventListener('click', () => {
// promise2
promise.resolve().then( _ => {
console.log('运行需要4ms')
// promise3
promise.resolve().then( _ => {
console.log('运行需要4ms')
})
})
})
</script>
2.jpg
其实只是在宏任务之后加入了一个时间段处理为任务.。在第一循环中处理btn1事件处理器时添加promise1到微任务队列中,在执行完btn1事件之后检查微任务队列发现有一个任务待处理,然后取出处理之后移除微任务promise1在循环微任务队列,发现以微任务队列为空,28ms时浏览器可以重新渲染(此时浏览器距离上次渲染相差12ms,渲染流畅)。在执行第二次事件循环的时候,在执行到微任务队列时,需要处理两个任务,耗时8ms,46ms时浏览器可以重新渲染(此时浏览器距离上次渲染相差18ms,渲染流畅)。
总结:如果在事件循环中有的任务比较耗时,会导致浏览器渲染fps变小,从而导致用户体验差。如向页面中插入10000个td节点的时候,如果是一次循环就appendChild一次,那相当于总共会有10000个微任务待执行,这样就会影响到浏览器渲染的fps,所有会出现点击页面没有反响的体验,这就是为什么插入多个节点需要拼接到一起一次性插入节点。了解事件循环和浏览器渲染之间到联系之后,以后开发多注意一些耗时任务到处理,到此分享结束。
网友评论