阮老師在其推特上放了一道題:
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => console.log(2));
console.log(4)
}).then(t => console.log(t));
console.log(3);
看到此處的你可以先猜測(cè)下其答案诀诊,然后再在瀏覽器的控制臺(tái)運(yùn)行這段代碼细办,看看運(yùn)行結(jié)果是否和你的猜測(cè)一致懒熙。
事件循環(huán)
眾所周知昼弟,JavaScript 語(yǔ)言的一大特點(diǎn)就是單線程啤它,也就是說(shuō),同一個(gè)時(shí)間只能做一件事。根據(jù) HTML 規(guī)范:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
為了協(xié)調(diào)事件变骡、用戶交互离赫、腳本、UI 渲染和網(wǎng)絡(luò)處理等行為锣光,防止主線程的不阻塞笆怠,Event Loop 的方案應(yīng)用而生。Event Loop 包含兩類:一類是基于 Browsing Context誊爹,一種是基于 Worker蹬刷。二者的運(yùn)行是獨(dú)立的,也就是說(shuō)频丘,每一個(gè) JavaScript 運(yùn)行的"線程環(huán)境"都有一個(gè)獨(dú)立的 Event Loop办成,每一個(gè) Web Worker 也有一個(gè)獨(dú)立的 Event Loop。
本文所涉及到的事件循環(huán)是基于 Browsing Context搂漠。
那么在事件循環(huán)機(jī)制中迂卢,又通過(guò)什么方式進(jìn)行函數(shù)調(diào)用或者任務(wù)的調(diào)度呢?
任務(wù)隊(duì)列
根據(jù)規(guī)范桐汤,事件循環(huán)是通過(guò)任務(wù)隊(duì)列的機(jī)制來(lái)進(jìn)行協(xié)調(diào)的而克。一個(gè) Event Loop 中,可以有一個(gè)或者多個(gè)任務(wù)隊(duì)列(task queue)怔毛,一個(gè)任務(wù)隊(duì)列便是一系列有序任務(wù)(task)的集合员萍;每個(gè)任務(wù)都有一個(gè)任務(wù)源(task source),源自同一個(gè)任務(wù)源的 task 必須放到同一個(gè)任務(wù)隊(duì)列拣度,從不同源來(lái)的則被添加到不同隊(duì)列碎绎。
在事件循環(huán)中,每進(jìn)行一次循環(huán)操作稱為 tick抗果,每一次 tick 的任務(wù)處理模型是比較復(fù)雜的筋帖,但關(guān)鍵步驟如下:
- 在此次 tick 中選擇最先進(jìn)入隊(duì)列的任務(wù)(oldest task),如果有則執(zhí)行(一次)
- 檢查是否存在 Microtasks冤馏,如果存在則不停地執(zhí)行日麸,直至清空 Microtasks Queue
- 更新 render
- 主線程重復(fù)執(zhí)行上述步驟
仔細(xì)查閱規(guī)范可知,異步任務(wù)可分為task 和 microtask 兩類(requestAnimationFrame 既不屬于 macrotask, 也不屬于 microtask)宿接,不同的API注冊(cè)的異步任務(wù)會(huì)依次進(jìn)入自身對(duì)應(yīng)的隊(duì)列中落蝙,然后等待 Event Loop 將它們依次壓入執(zhí)行棧中執(zhí)行在抛。
查閱了網(wǎng)上比較多關(guān)于事件循環(huán)介紹的文章岗钩,均會(huì)提到 macrotask(宏任務(wù)) 和 microtask(微任務(wù)) 兩個(gè)概念次洼,但規(guī)范中并沒有提到 macrotask爷抓,因而一個(gè)比較合理的解釋是 task 即為其它文章中的 macrotask侯谁。另外在 ES2015 規(guī)范中稱為 microtask 又被稱為 Job妥曲。
(macro)task主要包含:script(整體代碼)垦写、setTimeout蚣旱、setInterval碑幅、I/O戴陡、UI交互事件、postMessage沟涨、MessageChannel恤批、setImmediate(Node.js 環(huán)境)
microtask主要包含:Promise.then、MutaionObserver裹赴、process.nextTick(Node.js 環(huán)境)
在 Node 中喜庞,會(huì)優(yōu)先清空 next tick queue,即通過(guò)process.nextTick 注冊(cè)的函數(shù)棋返,再清空 other queue延都,常見的如Promise;此外睛竣,timers(setTimeout/setInterval) 會(huì)優(yōu)先于 setImmediate 執(zhí)行晰房,因?yàn)榍罢咴?timer 階段執(zhí)行,后者在 check 階段執(zhí)行射沟。
setTimeout/Promise 等API便是任務(wù)源殊者,而進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)。來(lái)自不同任務(wù)源的任務(wù)會(huì)進(jìn)入到不同的任務(wù)隊(duì)列验夯。其中setTimeout與setInterval是同源的猖吴。
示例
純文字表述確實(shí)有點(diǎn)干澀,這一節(jié)通過(guò)一個(gè)示例來(lái)逐步理解:
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
首先簿姨,事件循環(huán)從宏任務(wù)(macrotask)隊(duì)列開始距误,這個(gè)時(shí)候,宏任務(wù)隊(duì)列中扁位,只有一個(gè)script(整體代碼)任務(wù)准潭;當(dāng)遇到任務(wù)源(task source)時(shí),則會(huì)先分發(fā)任務(wù)到對(duì)應(yīng)的任務(wù)隊(duì)列中去域仇。所以刑然,上面例子的第一步執(zhí)行如下圖所示:
然后遇到了 console 語(yǔ)句,直接輸出 script start暇务。輸出之后泼掠,script 任務(wù)繼續(xù)往下執(zhí)行,遇到 setTimeout垦细,其作為一個(gè)宏任務(wù)源择镇,則會(huì)先將其任務(wù)分發(fā)到對(duì)應(yīng)的隊(duì)列中:
script 任務(wù)繼續(xù)往下執(zhí)行,遇到 Promise 實(shí)例括改。Promise 構(gòu)造函數(shù)中的第一個(gè)參數(shù)腻豌,是在 new 的時(shí)候執(zhí)行,構(gòu)造函數(shù)執(zhí)行時(shí),里面的參數(shù)進(jìn)入執(zhí)行棧執(zhí)行吝梅;而后續(xù)的 .then 則會(huì)被分發(fā)到 microtask 的 Promise 隊(duì)列中去虱疏。所以會(huì)先輸出 promise1,然后執(zhí)行 resolve苏携,將 then1 分配到對(duì)應(yīng)隊(duì)列做瞪。
構(gòu)造函數(shù)繼續(xù)往下執(zhí)行,又碰到 setTimeout右冻,然后將對(duì)應(yīng)的任務(wù)分配到對(duì)應(yīng)隊(duì)列:
script任務(wù)繼續(xù)往下執(zhí)行装蓬,最后只有一句輸出了 script end,至此国旷,全局任務(wù)就執(zhí)行完畢了矛物。
根據(jù)上述,每次執(zhí)行完一個(gè)宏任務(wù)之后跪但,會(huì)去檢查是否存在 Microtasks履羞;如果有,則執(zhí)行 Microtasks 直至清空 Microtask Queue屡久。
因而在script任務(wù)執(zhí)行完畢之后忆首,開始查找清空微任務(wù)隊(duì)列。此時(shí)被环,微任務(wù)中糙及,只有 Promise 隊(duì)列中的一個(gè)任務(wù) then1,因此直接執(zhí)行就行了筛欢,執(zhí)行結(jié)果輸出 then1浸锨。當(dāng)所有的 microtast 執(zhí)行完畢之后,表示第一輪的循環(huán)就結(jié)束了版姑。
這個(gè)時(shí)候就得開始第二輪的循環(huán)柱搜。第二輪循環(huán)仍然從宏任務(wù) macrotask開始。此時(shí)剥险,有兩個(gè)宏任務(wù):timeout1 和 timeout2聪蘸。
取出 timeout1 執(zhí)行,輸出 timeout1表制。此時(shí)微任務(wù)隊(duì)列中已經(jīng)沒有可執(zhí)行的任務(wù)了健爬,直接開始第三輪循環(huán):
第三輪循環(huán)依舊從宏任務(wù)隊(duì)列開始。此時(shí)宏任務(wù)中只有一個(gè) timeout2么介,取出直接輸出即可娜遵。
這個(gè)時(shí)候宏任務(wù)隊(duì)列與微任務(wù)隊(duì)列中都沒有任務(wù)了,所以代碼就不會(huì)再輸出其他東西了壤短。那么例子的輸出結(jié)果就顯而易見:
script start
promise1
script end
then1
timeout1
timeout2
總結(jié)
在回頭看本文最初的題目:
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => {
// t2
console.log(2)
});
console.log(4)
}).then(t => {
// t1
console.log(t)
});
console.log(3);
這段代碼的流程大致如下:
- script 任務(wù)先運(yùn)行魔熏。首先遇到 Promise 實(shí)例衷咽,構(gòu)造函數(shù)首先執(zhí)行鸽扁,所以首先輸出了 4蒜绽。此時(shí) microtask 的任務(wù)有 t2 和 t1
- script 任務(wù)繼續(xù)運(yùn)行,輸出 3桶现。至此躲雅,第一個(gè)宏任務(wù)執(zhí)行完成。
- 執(zhí)行所有的微任務(wù)骡和,先后取出 t2 和 t1相赁,分別輸出 2 和 1
- 代碼執(zhí)行完畢
綜上,上述代碼的輸出是:4321
為什么 t2 會(huì)先執(zhí)行呢慰于?理由如下:
- 根據(jù) Promises/A+規(guī)范:
實(shí)踐中要確保 onFulfilled 和 onRejected 方法異步執(zhí)行钮科,且應(yīng)該在 then 方法被調(diào)用的那一輪事件循環(huán)之后的新執(zhí)行棧中執(zhí)行
- Promise.resolve 方法允許調(diào)用時(shí)不帶參數(shù),直接返回一個(gè)resolved 狀態(tài)的 Promise 對(duì)象婆赠。立即 resolved 的 Promise 對(duì)象绵脯,是在本輪“事件循環(huán)”(event loop)的結(jié)束時(shí),而不是在下一輪“事件循環(huán)”的開始時(shí)休里。
http://es6.ruanyifeng.com/#docs/promise#Promise-resolve
所以蛆挫,t2 比 t1 會(huì)先進(jìn)入 microtask 的 Promise 隊(duì)列。
這段解釋更清晰:
一旦一個(gè)pormise有了結(jié)果妙黍,或者早已有了結(jié)果悴侵,他就會(huì)為它的回調(diào)產(chǎn)生一個(gè)微任務(wù)。如果在微任務(wù)執(zhí)行期間微任務(wù)隊(duì)列加入了新的微任務(wù)拭嫁,會(huì)將新的微任務(wù)加入隊(duì)列尾部可免,之后也會(huì)被執(zhí)行。
當(dāng)執(zhí)行resolve(1)的時(shí)候做粤,代碼還沒運(yùn)行到then(t => {console.log(t)})浇借,這時(shí)候是沒有回調(diào)的,所以這時(shí)候還是沒有添加任何微任務(wù)的驮宴。
接下來(lái)執(zhí)行Promise.resolve().then(t => {console.log(2)})逮刨,為已有結(jié)果的內(nèi)層Promise添加一個(gè)微任務(wù),然后外層Promise執(zhí)行.then(t => {console.log(t)})堵泽,這時(shí)候外層Promise是屬于早已有了結(jié)果修己,所以為這個(gè)回調(diào)添加一個(gè)微任務(wù)。
輸出2的微任務(wù)在輸出1的微任務(wù)前面迎罗,所以是先輸出 2 再輸出 1
看看你掌握了沒
再來(lái)一個(gè)題目睬愤,來(lái)做個(gè)練習(xí):
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
這個(gè)題目就稍微有點(diǎn)復(fù)雜了,我們?cè)俜治鱿拢?/p>
首先纹安,事件循環(huán)從宏任務(wù)(macrotask)隊(duì)列開始尤辱,最初始砂豌,宏任務(wù)隊(duì)列中,只有一個(gè)script(整體代碼)任務(wù)光督;當(dāng)遇到任務(wù)源(task source)時(shí)阳距,則會(huì)先分發(fā)任務(wù)到對(duì)應(yīng)的任務(wù)隊(duì)列中去。所以结借,就和上面例子類似筐摘,首先遇到了console.log,輸出script start船老;
接著往下走咖熟,遇到setTimeout任務(wù)源,將其分發(fā)到任務(wù)隊(duì)列中去柳畔,記為timeout1馍管;
接著遇到promise,new promise中的代碼立即執(zhí)行薪韩,輸出promise1,然后執(zhí)行resolve,遇到setTimeout,將其分發(fā)到任務(wù)隊(duì)列中去确沸,記為timemout2,將其then分發(fā)到微任務(wù)隊(duì)列中去,記為then1躬存;
接著遇到console.log代碼张惹,直接輸出script end
接著檢查微任務(wù)隊(duì)列,發(fā)現(xiàn)有個(gè)then1微任務(wù)岭洲,執(zhí)行宛逗,輸出then1
再檢查微任務(wù)隊(duì)列,發(fā)現(xiàn)已經(jīng)清空盾剩,則開始檢查宏任務(wù)隊(duì)列雷激,執(zhí)行timeout1,輸出timeout1;
接著執(zhí)行timeout2告私,輸出timeout2
至此屎暇,所有的都隊(duì)列都已清空,執(zhí)行完畢驻粟。其輸出的順序依次是:script start, promise1, script end, then1, timeout1, timeout2