记得第一次面鹅厂的时候,懵懂的我刚做了一两个前端项目(还是没用框架裸写的那种),没怎么学过就去面了。
面试官问我是不是有了解Promise
,我在项目中有用过,就回答了解过,没想到出了一道题就让我踩进坑了:(其实这道题的根本是在考事件循环 event loop
)
// 问输出顺序是什么
setTimeout(function(){
console.log(1)
});
new Promise(function(resolve){
console.log(2)
resolve();
}).then(function(){
console.log(3)
});
console.log(4);
萌新的我答 2 3 1 4
(虽然现在依然很萌新),实际上呢应该是2 4 3 1
。
一、Event Loop
Event Loop是一个执行模型,在不同的地方有不同的实现。
浏览器和nodejs基于不同的技术实现了各自的Event Loop。目前本文仅讨论浏览器的情况,以后再讨论nodejs的情况。浏览器的Event Loop在HTML5规范已经明确定义了。
二、为什么要有Event Loop?
因为,Javascript本质是单线程的,为了主线程不阻塞和实现一些类似多线程的操作,我们就需要Event Loop这个执行模型。
三、宏队列和微队列
队列我们知道,是先进先出的(FIFO),因此,异步任务的回调会依次进入任务队列,等到后续被调用。
1. 宏队列(task queue)
包括整体的代码script
包括以下的异步任务:
- setTimeout
- setInterval
- setImmediate (nodejs)
- requestAnimationFrame
- I/O
- UI rendering
2. 微队列(microtask queue)
包括以下异步任务:
- process.nextTick (nodejs)
- Promise
- Object.observe
- MutationObserver
四、Event Loop 运行原理
- 全局script代码执行完毕后,调用栈stack会清空;
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈stack中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈stack中执行,以此类推,直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈stack也为空;
- 取出宏队列task queue中位于队首的任务,放入Stack中执行;
- 执行完毕后,调用栈stack为空;
- 重复 2 - 6 步骤。
上面是详细分析,简单总结用大白话来说:
- 执行完全局script代码
- 清空执行完微队列
- 取宏队列队首任务,执行
- 清空执行完微队列
- 取宏队列队首任务,执行
……
而且要注意,对于setTimeout
和setInterval
这些有计时的任务,是在计时结束后才加进任务队列的。
五、问题实操:
// 问输出顺序是什么
setTimeout(function(){
console.log(1)
});
new Promise(function(resolve){
console.log(2)
resolve();
}).then(function(){
console.log(3)
});
console.log(4);
回到文首的问题,我们用事件循环来分析:
- 执行宏任务整体的
script
代码,setTimeout
加入到宏队列,new Promise
执行,输出2
,并且将then
分发到微队列,执行console.log(4)
输出4
;
宏队列:[setTimeout]
微队列:[then]
输出:2 4
- 把所有微队列的任务都取出执行,这里只有一个
then
,所以输出3
宏队列:[setTimeout]
微队列:[]
输出:2 4 3
- 取出宏队列队首
setTimeout
,执行输出1
宏队列:[]
微队列:[]
输出:2 4 3 1
执行完毕,输出2 4 3 1
将来会再加一个复杂一点的例子。