异步与事件循环
前言
在前端面试中经常会遇到代码执行顺序的问题,搞清楚异步与事件循环的机制,可以对此类问题一通百通。
一、术语
1.1 同步 synchronous
众所周知“JavaScript是单线程的”。JavaScript只有一个主线程,负责解释和和执行JavaScript代码。
JavaScript主线程会按顺序去执行执行环境栈内的代码。当遇到耗时较长的任务时就会产生阻塞,当遇到死循环时会直接卡死,无法继续执行。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程。1
1.2 异步 asynchronous
由于CPU执行速度很快,IO操作较慢。所有JavaScript设计为对于IO操作或计时任务都交由浏览器的web APIs或 node的C/C++ APIs去处理。等到处理完毕后再交到主线程去处理后续任务。
1.3 任务队列 task queue
通常来说在编码过程中常常需要在异步操作处理完成后去执行对应任务。JavaScript会在异步操作完成后将对应的任务放到任务队列里面。等到主线程空闲时再去执行。
任务队列分为macrotasks和microtasks两种。
macrotasks
- setTimeout
- setInterval
- setImmediate (node)
- requestAnimationFrame (浏览器)
- I/O
- UI rendering (浏览器)
microtasks
- process.nextTick (node)
- Promises
- Object.observe (已废弃)
- MutationObserver(浏览器)
1.4 事件循环
事件循环决定了JavaScript的执行顺序。
事件循环的顺序为
- 从上往下执行JavaScript内的代码。
- 遇到macrotasks或microtasks方法后视不同环境交由浏览器的web APIs或 node的C/C++ APIs处理 2。
- web APIs或 node的C/C++ APIs处理完成后将microtasks方法对应的任务推入到microtasks队列,将macrotasks方法对应的任务推入到macrotasks队列。
- 主程序执行完毕后,即执行环境栈只剩下全局执行环境时,开始执行microtasks队列内的全部任务。然后取macrotasks队列内的一个任务执行,再执行microtasks队列内的全部任务(如果有)。再取macrotasks队列内的下一个任务执行,再执行microtasks队列内的全部任务。
- 重复循环以上步骤。
二、 特殊情况
2.1 process.nextTick
- process.nextTick的回调函数会在同一次循环内的任何其他异步回调函数之前执行。
- process.nextTick回调函数内部调用的process.nextTick仍会在同一阶段执行。递归调用的process.nextTick会阻断事件循环,使其无法进入下一阶段。
process.nextTick(function() {
console.log(111)
process.nextTick(function() {
console.log(222)
})
})
setImmediate(function () {
console.log(333)
})
// -> 111 222 333
2.2 setImmediate
setImmediate 和 setTimeout(function() {}, 0)很类似。
当这两者在非异步回调函数的执行环境内执行时,其执行先后顺序不确定。 在异步回调函数的执行环境内执行时setImmediate总会先于setTimeout(function() {}, 0)执行。
setImmediate(function () {
console.log(111)
})
setTimeout(function () {
console.log(222)
})
// 执行顺序不确定,结果可能是111 222或222 111
setTimeout(function test () {
setImmediate(function () {
console.log(111)
})
setTimeout(function () {
console.log(222)
})
}, 0)
// 执行顺序确定,总是 111 222
2.3 UI渲染时机
通过Javascript代码更新后的UI渲染会在microtasks之后,macrotasks之前进行。 3
<div id="div">
begin
</div>
<script>
div.onclick = function () {
div.innerHTML = 'end';
setTimeout(function() {
alert(' ui 已经渲染完毕 ');
console.log('timeout');
}, 0);
new Promise(function(resolve) {
console.log('promise1');
for (var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
alert(' ui 开始渲染 ');
});
console.log('global');
}
// 执行顺序为 promise1, promise2, global, then1, ui 开始渲染, div内文字变为end, ui 已经渲染完毕, timeout
</script>
三、 总结
经过多天的查询资料,基本搞清楚了事件循环的执行步骤,彻底掌握了这些异步代码的执行顺序问题。
但是在一些细节上仍留有疑惑,如process.nextTick的回调函数是先放入任务队列等待执行还是直接放入执行环境栈直接执行。关于setImmediate和setTimeout(function() {}, 0)执行顺序不确定的原因也未找到较好的解释。等以后找到资料具体理解之后再做补充。
下篇文章分析一下Promise,看看这个最初由社区实现的功能代码是如何写的。
四、 参考
[1] JavaScript 运行机制详解:再谈Event Loop
[2] 理解异步JavaScript-事件循环
[3] Javascript事件循环机制以及渲染引擎何时渲染UI