先看一段代码,也是一道经典面试题:
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
其输出结果为:
// 1
// 2
// 3
// 5
// 4
我们知道,JavaScript 在同一时间片内只能执行一个任务:
主线程会依次执行代码,当执行到函数的时候会将函数加入执行栈,当函数执行完毕后再将其出栈,直至代码执行完毕。当执行栈为空时,runtime 会从任务队列(先入先出)中取出待执行的回调函数并执行,入栈、出栈的过程同上。这个机制就叫做 Event Loop。
由此可以认为,Event Loop实际上是一系列的回调函数集合。
举例来说:在浏览器中,对于网络请求等需要等待一段时间才会返回结果的操作,我们通常采用异步回调来处理,这个回调就会放入任务队列中。此时浏览器会在其它线程中执行异步操作,操作完成后将回调函数放入主线程任务队列中。Event Loop负责在主线程执行完毕后将任务队列中的函数放入执行栈中,由主线程执行。当主线程将执行栈中的函数执行完毕后,再次读取任务队列,形成循环。所以即使主线程阻塞了,任务队列依然能够被添加函数,因为任务队列的添加是由浏览器负责的。(不同的 runtime 实现可能不同)
另外需要注意的是 Promise.then 是异步执行的,而创建 Promise 实例是同步执行的。这就解释了为什么1、2、3输出在4、5之前。
但为什么5 会输出在4前面呢?
JavaScript 中的任务又分为MacroTask 与 MicroTask 两种。
典型的 MacroTask 包含了 :
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- I/O
- UI rendering
而常见的MicroTask 包含了
- process.nextTick
- Promises
- Object.observe
- MutationObserver
Event Loop 中有一个或多个Task Queue,即MacroTask Queue,仅有一个Job Queue,即MicroTask Queue。Task Queue的执行是按照回调顺序先入先出,而在 MacroTask 的执行间隙中会清空已有的 MicroTask Queue
回到代码中,setTimeout(function() {console.log(4)}, 0); 既然延迟设置为0,为什么5会在4之前输入呢?
那是因为setTimeout设置为0的时候,runtime其实并不是0,在主流浏览器中会将其设置为4,而 node 则会将其设置为1。那么现在代码的执行顺序就很清晰了:
console.log(1); // 创建 Promise 主线程执行
...
console.log(2); // 创建 Promise 主线程执行
console.log(3); // test 函数立即执行, 主线程执行
...
console.log(5); // 主线程执行完毕,执行MicroTask Queue,即 promise.then
...
console.log(4); // 执行 setTimeout(4)
参考资料:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。