前几天面了几场试,每次面试都有问到浏览器的事件循环,所以想着写这篇文章总结下。
思考题:
setTimeout,promise,requestAnimationFrame执行顺序是咋样的呢?
What
首先我们得明白什么是事件循环:
- 在计算机科学中,事件循环是一种程序构造或设计模式,用于等待并调度程序中的事件或消息。
- 它的工作方式是向某个内部或外部“事件提供程序”发出请求(通常将请求阻止,直到事件到达),然后调用相关的事件处理程序(“调度事件”)
- 事件循环有时也称为消息分发程序,消息循环,消息泵或运行循环。
//事件循环框架
function main()
messageTask = []
while(messageTask.length !== 0){
task = messageTask.pop()
execute(task)
}
Why
为什么浏览器会采用事件循环呢?这是因为事件循环这种模式最符合浏览器渲染线程的工作,浏览器会不停的执行渲染事件,时间间隔大约1秒60次,其中刷新的间隔基本上可以满足大部分普通的任务执行。而且页面在运行中会不断的加入新的任务,比如点击事件,xhr,setTimeout等,需要不断判定是否有新的任务出现,事件循环机制能解决该问题。
// 浏览器事件循环
// 版本一 消息队列
while(true){
queue = getNextQueue()
task = queue.pop()
exexute(task)
if(isRepaintTime()){
repaint()
}
}
How
众所周知,浏览器除了同步任务之外,还有类似setTimeout,XHR等webapis,以及promise,MutationObserver等微任务,还有requestAnimationFrame。对于这些异步任务的话浏览器是如何处理的呢?
webapis
比较常见的setTimeout 是直接将延迟任务添加到延迟队列中,等到指定时间再将其移至消息队列执行。而 XMLHttpRequest,是由浏览器的网络进程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。
消息队列的任务称为宏任务,宏任务的时间粒度比较大,实行性会较低。
微任务
微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。它的出现主要是为了解决任务实时性问题。因为我们不能保证主函数执行期间渲染进程不会把其它任务放进消息队列中。
想象下如果你希望在主函数执行之后执行你的回调方法,使用宏任务把回调方法放消息队列里的话会怎么样?
// 浏览器事件循环
// 版本二 消息队列 + 微任务
while(true){
queue = getNextQueue()
task = queue.pop()
exexute(task)
//每一轮宏任务执行后都会把微任务队列清空
while(microTaskQueue.hasTask()){
doMicroTask()
}
if(isRepaintTime()){
repaint()
}
}
tips: 如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中, 也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。
requestAnimationFrame
现代浏览器的帧率一般为1秒60次,即16.7ms左右一次,为了让动画丝滑,最好的时间就是16.7ms左右进行一次动画的更新,所以才会有requestAnimationFrame出现,requestAnimationFrame的执行间隔与浏览器的帧间隔保持一致,它解决了setTimeout因消息队列任务过多导致的延时问题。
// 浏览器事件循环
// 版本二 消息队列 + 微任务 + requestAnimationFrame
while(true){
queue = getNextQueue()
task = queue.pop()
exexute(task)
while(microTaskQueue.hasTask()){
doMicroTask()
}
if(isRepaintTime()){
animationTasks = animationQueue.copy()
for(task of animationTasks){
doAnimation(task)
}
repaint()
}
}
// 从代码可以发现执行优先级
//promise => requestAnimationFrame => setTimeout
后记
文章篇幅不长,看代码感觉比看文字好理解。还有下面这几篇视频讲解是真的棒~强烈推荐~