事件循环
js 是单线程语言,浏览器中 js 主线程会阻塞渲染,因此需要将一些耗时的任务放到别的线程去作为异步任务执行
浏览器为了在主线程中能够协调各种异步事件(网络、用户操作),引入了事件循环模型
在事件循环中,我们将待执行的任务分为两大类,有两个对应的队列负责存放这两种任务:
- 宏任务:setTimeout、setInterval、setImmediate、requestAnimationFrame、I/O、UI 渲染、用户交互
- 微任务:process.nextTick、Promise、MutationObserver、queueMicrotask
事件循环大致流程:
- 浏览器在执行我们代码的过程中,会将生成的任务加入其对应的队列
- 代码执行完毕后(调用栈为空),循环取出微任务队列中最早的任务并放入调用栈执行,直到该队列为空
- 取出一个宏任务队列中最早的任务并放入调用栈执行,执行完毕(调用栈为空)后回到第二步
任务执行
任务执行的时候也能产生任务,如果递归执行微任务,会阻塞整个页面
requestAnimationFrame
这个方法产生的宏任务有些特殊,可以认为这类任务有一个专用的队列,我暂且称为 帧任务队列
它会在每一帧开始渲染前复制帧任务队列中的所有任务并执行,从任务调度来看,它的执行顺序是:微任务 -> 帧任务 -> 其它宏任务
执行所有帧任务
执行的帧任务队列是当前的帧任务队列的拷贝
这是为了将帧任务中生成的帧任务放到下一帧执行
因此,事件循环可以用以下伪代码表示:
while(true){
// 从任务队列中取最旧的宏任务执行
task = queue.pop();
execute(task);
// 检测微任务队列是否有微任务,如果有依次执行,直到微任务队列为空
while(microtaskQueue.hasTasks(){
doMicroTask();
}
// 是否到即将渲染下一帧
if(isRepaintTime()){
// 执行动画队列中的任务
frameTasks = frameQueue.copyTasks();
frameQueue.clearTasks();
for(task in animationTasks){
doAnimationTask(task);
}
// 渲染页面
repaint();
}
}
先执行宏任务?
浏览器执行我们的代码,就是一个宏任务,因此是先检查宏任务队列
调用栈
js 使用栈先进后出的特性来保证代码的执行顺序,例如下面代码:
function foo() {
console.log('foo');
}
function bar() {
foo();
console.log('bar');
}
bar();
- 执行
bar()
,入栈:[bar()]
- 执行
bar
中的foo()
,入栈:[bar(), foo()]
- 执行
foo
中的console.log('foo')
,入栈:[bar(), foo(), console.log('foo')]
console.log('foo')
执行完毕,出栈:[bar(), foo()]
foo()
执行完毕,出栈:[bar()]
- 执行
foo
中的console.log('bar')
,入栈:[bar(), console.log('bar')]
console.log('bar')
执行完毕,出栈:[bar()]
bar()
执行完毕,出栈:[]
具体可以看这个在线例子