各位读者可以先思考一下这道题的输出结果是什么?我第一次答这题是错了。。。
console.log('start');
Promise.resolve()
.then(() => {
console.log('p1');
}).then(() => {
console.log('p2');
});
(new Promise(resolve => {
console.log('p3')
resolve()
})).then(() => {
console.log('p4')
});
setTimeout(() => {
Promise.resolve()
.then(() => {
console.log('p5');
}).then(() => {
console.log('p6');
});
setInterval(() => {
console.log('interval');
},3000);
setTimeout(() => {
console.log('timeout1');
}, 0);
console.log('timeout2');
},0);
console.log('end')
EventLoop核心知识点
网上讲解EventLoop的文章数不胜数,可以阅读阮一峰老师的《再谈Event Loop》和creeperyang的《从Promise来看JavaScript中的Event Loop、Tasks和Microtasks》
进程与线程
进程:浏览器是多进程的,每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
线程:渲染进程又包括GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程
宏任务与微任务
宏任务:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
微任务:Promise、MutaionObserver、process.nextTick(Node.js 环境)
循环顺序
事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
浏览器单个Tick核心步骤
- 在此次 tick 中检查宏任务队列中的队头任务( oldest task,最先进入队列的任务 ),如果有则推入执行栈执行(一次)
- 检查微任务队列是否存在任务,如果存在则不停地推入执行栈执行,直至清空微任务队列
- 进入更新渲染阶段,判断是否需要渲染。这里有一个 rendering opportunity 的概念,也就是说不一定每一轮 event loop 都会对应一次浏览 器渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。(所以多个 task 很可能在一次渲染之间执行)
- 重复执行上述步骤
牢记一点
JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。
Promise核心知识点
推荐阅读《Promise的源码实现(完美符合Promise/A+规范)》
- 根据Promises/A+规范,then方法的两个回调函数需要放入异步任务中,如果模拟实现通常使用setTimeout放入宏任务中,但是平台实现则是放入微任务中。
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
- 只有当前promise处于非pending状态(即处于fulfilled或者rejected状态)时,才立即放入微任务队列中,否则等待Promise状态改变才推入微任务队列。
2.3.2 If x is a promise, adopt its stat:
2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected.
2.3.2.2 If/when x is fulfilled, fulfill promise with the same value.
2.3.2.3 If/when x is rejected, reject promise with the same reason.
- Promise构造函数执行器(executor)中的同步代码立即执行。
图解答案
脚本执行过程中关键点的执行上下文栈、宏任务队列、微任务队列情况。
-
脚本开始执行
-
执行
console.log('start')
,输出’start’。弹出执行栈
-
执行Promise.resolve().then(…)方法,因为promise已经是fulfilled状态,所以回调函数() => { console.log(‘p1’) }直接推入微任务队列中。此时第一个then回调函数并未为执行,对于第二个then方法而言,promise还处于pending状态,所以等待promise状态改变。
-
Promise构造函数中的同步代码立即执行
-
执行
console.log('p3')
,输出’p3’,promise变为fulfilled状态,then方法的回调函数放入微任务队列中
-
执行setTimeout定时器,放入定时器线程中执行,因为设置为0ms(如果定时器嵌套五层,最少4ms后才会执行,具体可阅读《为什么 setTimeout 有最小时延 4ms ?》),所以定时器线程0ms后就会将任务推入宏任务队列中。
-
执行
console.log('end')
,输出’end’
-
script脚本也属于宏任务,故当前tick下依次执行并清空微任务队列,首先执行队首的任务。输出’p1’,执行完成后Promise变成fulfilled状态,所以将第二个then()方法的回调函数放入微任务队列中。
-
继续执行微任务队列中任务,依次输出’p4’, ‘p2’
-
微任务清空完成,取出宏任务队列中队首任务放入执行栈中执行。
-
与上面第三步相似,第一个then方法的回调函数先推入微任务队列中
-
执行setInterval定时器,等待定时器每3秒将回调任务加入宏任务队列。
-
执行setTimeout定时器,回调函数在0ms后推入宏任务队列。
-
执行
console.log('timeout2')
,输出’timeout2’。
-
依次执行并清空微任务队列,与第8步同理。输出’p5’
-
继续执行微任务队列中任务,输出’p6’
-
微任务清空完成,取出宏任务队列中队首任务放入执行栈中执行,输出’timeout1’
-
约3秒,宏任务队列推入interval定时器回调函数,等待下一tick执行
动图效果如下:
所以最终输出如下:
start
p3
end
p1
p4
p2
timeout2
p5
p6
timeout1
interval
interval