JavaScript執(zhí)行過(guò)程(Event Loop)

阮老師在其推特上放了一道題:

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ù)可分為taskmicrotask 兩類(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);

這段代碼的流程大致如下:

  1. script 任務(wù)先運(yùn)行魔熏。首先遇到 Promise 實(shí)例衷咽,構(gòu)造函數(shù)首先執(zhí)行鸽扁,所以首先輸出了 4蒜绽。此時(shí) microtask 的任務(wù)有 t2 和 t1
  2. script 任務(wù)繼續(xù)運(yùn)行,輸出 3桶现。至此躲雅,第一個(gè)宏任務(wù)執(zhí)行完成。
  3. 執(zhí)行所有的微任務(wù)骡和,先后取出 t2 和 t1相赁,分別輸出 2 和 1
  4. 代碼執(zhí)行完畢

綜上,上述代碼的輸出是:4321

為什么 t2 會(huì)先執(zhí)行呢慰于?理由如下:

實(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末根悼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蜀撑,更是在濱河造成了極大的恐慌挤巡,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酷麦,死亡現(xiàn)場(chǎng)離奇詭異矿卑,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)沃饶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門母廷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)轻黑,“玉大人,你說(shuō)我怎么就攤上這事琴昆∶ケ桑” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵椎咧,是天一觀的道長(zhǎng)玖详。 經(jīng)常有香客問(wèn)我,道長(zhǎng)勤讽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任拗踢,我火速辦了婚禮脚牍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘巢墅。我一直安慰自己诸狭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布君纫。 她就那樣靜靜地躺著驯遇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蓄髓。 梳的紋絲不亂的頭發(fā)上叉庐,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音会喝,去河邊找鬼陡叠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛肢执,可吹牛的內(nèi)容都是我干的枉阵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼预茄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼兴溜!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起耻陕,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拙徽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后淮蜈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斋攀,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年梧田,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了淳蔼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侧蘸。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鹉梨,靈堂內(nèi)的尸體忽然破棺而出讳癌,到底是詐尸還是另有隱情,我是刑警寧澤存皂,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布晌坤,位于F島的核電站,受9級(jí)特大地震影響旦袋,放射性物質(zhì)發(fā)生泄漏骤菠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一疤孕、第九天 我趴在偏房一處隱蔽的房頂上張望商乎。 院中可真熱鬧,春花似錦祭阀、人聲如沸鹉戚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)抹凳。三九已至,卻和暖如春伦腐,著一層夾襖步出監(jiān)牢的瞬間赢底,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工蔗牡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留颖系,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓辩越,卻偏偏與公主長(zhǎng)得像嘁扼,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子黔攒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 弄懂js異步 講異步之前趁啸,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,711評(píng)論 0 5
  • 什么是事件循環(huán)(Event Loop) 事件循環(huán)能讓 Node.js 執(zhí)行非阻塞 I/O 操作督惰,盡管JavaScr...
    假面猿閱讀 785評(píng)論 0 0
  • JS中比較讓人頭疼的問(wèn)題之一要算異步事件了,比如我們經(jīng)常要等后臺(tái)返回?cái)?shù)據(jù)后進(jìn)行dom操作觉阅,又比如我們要設(shè)置一個(gè)定時(shí)...
    si_月閱讀 1,015評(píng)論 0 0
  • 歡迎光臨我的博客拓跋的前端客棧崖疤,如果您發(fā)現(xiàn)我文章中存在錯(cuò)誤秘车,請(qǐng)盡情向我吐槽,大家一起學(xué)習(xí)一起進(jìn)步φ(>ω<*) 1...
    zhleven閱讀 5,461評(píng)論 5 12
  • 白玉石欄恢廟宇劫哼,梵音始覺靈臺(tái) 真如是叮趴?強(qiáng)壘仙崍 朱門隔世閉,都以數(shù)春開 柳毅舊山橋任在权烧,失人故道情懷 晚春幕眯亦,雨意...
    孫若蘭閱讀 266評(píng)論 3 6