從一道題淺說 JavaScript 的事件循環(huán)
注:本篇文章運(yùn)行環(huán)境為當(dāng)前最新版本的谷歌瀏覽器(72.0.3626.109)
最近看到這樣一道有關(guān)事件循環(huán)的前端面試題:
//請(qǐng)寫出輸出內(nèi)容
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
這道題主要考察的是事件循環(huán)中函數(shù)執(zhí)行順序的問題辑莫,其中包括async
钙畔,await
煤率,setTimeout
坦喘,Promise
函數(shù)座享。下面來說一下本題中涉及到的知識(shí)點(diǎn)菱涤。
任務(wù)隊(duì)列
首先我們需要明白以下幾件事情:
- JS分為同步任務(wù)和異步任務(wù)
- 同步任務(wù)都在主線程上執(zhí)行绸栅,形成一個(gè)執(zhí)行棧
- 主線程之外,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列繁莹,只要異步任務(wù)有了運(yùn)行結(jié)果檩互,就在任務(wù)隊(duì)列之中放置一個(gè)事件。
- 一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時(shí)JS引擎空閑)咨演,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列闸昨,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中,開始執(zhí)行薄风。
根據(jù)規(guī)范饵较,事件循環(huán)是通過任務(wù)隊(duì)列的機(jī)制來進(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ì)列茄猫,從不同源來的則被添加到不同隊(duì)列。 setTimeout/Promise 等API便是任務(wù)源困肩,而進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)划纽。
宏任務(wù)
(macro)task(又稱之為宏任務(wù)),可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)锌畸。
瀏覽器為了能夠使得JS內(nèi)部(macro)task與DOM任務(wù)能夠有序的執(zhí)行勇劣,會(huì)在一個(gè)(macro)task執(zhí)行結(jié)束后,在下一個(gè)(macro)task 執(zhí)行開始前潭枣,對(duì)頁面進(jìn)行重新渲染比默,流程如下:
(macro)task->渲染->(macro)task->...
(macro)task主要包含:script(整體代碼)、setTimeout卸耘、setInterval退敦、I/O、UI交互事件蚣抗、postMessage侈百、MessageChannel、setImmediate(Node.js 環(huán)境)
微任務(wù)
microtask(又稱為微任務(wù))翰铡,可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)钝域。也就是說,在當(dāng)前task任務(wù)后锭魔,下一個(gè)task之前例证,在渲染之前。
所以它的響應(yīng)速度相比setTimeout(setTimeout是task)會(huì)更快迷捧,因?yàn)闊o需等渲染织咧。也就是說胀葱,在某一個(gè)macrotask執(zhí)行完后,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)笙蒙。
microtask主要包含:Promise.then抵屿、MutaionObserver、process.nextTick(Node.js 環(huán)境)
運(yùn)行機(jī)制
在事件循環(huán)中捅位,每進(jìn)行一次循環(huán)操作稱為 tick轧葛,每一次 tick 的任務(wù)處理模型是比較復(fù)雜的,但關(guān)鍵步驟如下:
- 執(zhí)行一個(gè)宏任務(wù)(棧中沒有就從事件隊(duì)列中獲韧Р蟆)
- 執(zhí)行過程中如果遇到微任務(wù)尿扯,就將它添加到微任務(wù)的任務(wù)隊(duì)列中
- 宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
- 當(dāng)前宏任務(wù)執(zhí)行完畢焰雕,開始檢查渲染衷笋,然后GUI線程接管渲染
- 渲染完畢后,JS線程繼續(xù)接管淀散,開始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲扔依场)
流程圖如下:
Promise和async中的立即執(zhí)行
我們知道Promise中的異步體現(xiàn)在then
和catch
中蚜锨,所以寫在Promise中的代碼是被當(dāng)做同步任務(wù)立即執(zhí)行的档插。而在async/await中,在出現(xiàn)await出現(xiàn)之前亚再,其中的代碼也是立即執(zhí)行的郭膛。那么出現(xiàn)了await時(shí)候發(fā)生了什么呢?
await做了什么
從字面意思上看await就是等待氛悬,await 等待的是一個(gè)表達(dá)式则剃,這個(gè)表達(dá)式的返回值可以是一個(gè)promise對(duì)象也可以是其他值。
很多人以為await會(huì)一直等待之后的表達(dá)式執(zhí)行完之后才會(huì)繼續(xù)執(zhí)行后面的代碼如捅,實(shí)際上await是一個(gè)讓出線程的標(biāo)志棍现。await后面的表達(dá)式會(huì)先執(zhí)行一遍,將await后面的代碼加入到microtask中镜遣,然后就會(huì)跳出整個(gè)async函數(shù)來執(zhí)行后面的代碼己肮。
這里感謝@chenjigeng的糾正:
由于因?yàn)閍sync await 本身就是promise+generator的語法糖。所以await后面的代碼是microtask悲关。所以對(duì)于本題中的
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
等價(jià)于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
回到本題
以上就本道題涉及到的所有相關(guān)知識(shí)點(diǎn)了谎僻,下面我們?cè)倩氐竭@道題來一步一步看看怎么回事兒。
-
首先寓辱,事件循環(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í)行如下圖所示:
-
然后我們看到首先定義了兩個(gè)async函數(shù)肋乍,接著往下看,然后遇到了
console
語句敷存,直接輸出script start
墓造。輸出之后,script 任務(wù)繼續(xù)往下執(zhí)行锚烦,遇到setTimeout
觅闽,其作為一個(gè)宏任務(wù)源,則會(huì)先將其任務(wù)分發(fā)到對(duì)應(yīng)的隊(duì)列中: -
script 任務(wù)繼續(xù)往下執(zhí)行涮俄,執(zhí)行了async1()函數(shù)蛉拙,前面講過async函數(shù)中在await之前的代碼是立即執(zhí)行的,所以會(huì)立即輸出
async1 start
彻亲。遇到了await時(shí)孕锄,會(huì)將await后面的表達(dá)式執(zhí)行一遍,所以就緊接著輸出
async2
苞尝,然后將await后面的代碼也就是console.log('async1 end')
加入到microtask中的Promise隊(duì)列中畸肆,接著跳出async1函數(shù)來執(zhí)行后面的代碼。 -
script任務(wù)繼續(xù)往下執(zhí)行宙址,遇到Promise實(shí)例轴脐。由于Promise中的函數(shù)是立即執(zhí)行的,而后續(xù)的
.then
則會(huì)被分發(fā)到 microtask 的Promise
隊(duì)列中去抡砂。所以會(huì)先輸出promise1
大咱,然后執(zhí)行resolve
,將promise2
分配到對(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ù)async1 end
和promise2
贸人,因此按先后順序輸出async1 end,promise2
佃声。當(dāng)所有的 Microtasks 執(zhí)行完畢之后艺智,表示第一輪的循環(huán)就結(jié)束了。 第二輪循環(huán)開始圾亏,這個(gè)時(shí)候就會(huì)跳回async1函數(shù)中執(zhí)行后面的代碼十拣,然后遇到了同步任務(wù)console
語句,直接輸出async1 end
志鹃。這樣第二輪的循環(huán)就結(jié)束了夭问。(也可以理解為被加入到script任務(wù)隊(duì)列中,所以會(huì)先與setTimeout隊(duì)列執(zhí)行)第二輪循環(huán)依舊從宏任務(wù)隊(duì)列開始曹铃。此時(shí)宏任務(wù)中只有一個(gè)
setTimeout
缰趋,取出直接輸出即可,至此整個(gè)流程結(jié)束陕见。
下面我會(huì)改變一下代碼來加深印象秘血。
變式一
在第一個(gè)變式中我將async2中的函數(shù)也變成了Promise函數(shù),代碼如下:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
可以先自己看看輸出順序會(huì)是什么评甜,下面來公布結(jié)果:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
在第一次macrotask執(zhí)行完之后灰粮,也就是輸出script end
之后,會(huì)去清理所有microtask蜕着。所以會(huì)相繼輸出promise2
谋竖,async1 end
承匣,promise4
,其余不再多說零聚。
變式二
在第二個(gè)變式中袍暴,我將async1中await后面的代碼和async2的代碼都改為異步的,代碼如下:
async function async1() {
console.log('async1 start');
await async2();
//更改如下:
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
//更改如下:
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
可以先自己看看輸出順序會(huì)是什么隶症,下面來公布結(jié)果:
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
在輸出為promise2
之后政模,接下來會(huì)按照加入setTimeout隊(duì)列的順序來依次輸出,通過代碼我們可以看到加入順序?yàn)?code>3 2 1蚂会,所以會(huì)按3淋样,2,1的順序來輸出胁住。
變式三
變式三是我在一篇面經(jīng)中看到的原題趁猴,整體來說大同小異儡司,代碼如下:
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
無非是在微任務(wù)那塊兒做點(diǎn)文章娱挨,前面的內(nèi)容如果你都看懂了的話這道題一定沒問題的,結(jié)果如下:
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
轉(zhuǎn)載自:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7 如有侵權(quán)捕犬,請(qǐng)聯(lián)系刪除