從一道題淺說 JavaScript 的事件循環(huán)

原文鏈接: https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7

注:本篇文章運行環(huán)境為當前最新版本的谷歌瀏覽器(72.0.3626.109)

任務隊列

首先我們需要明白以下幾件事情:

  • JS分為同步任務和異步任務
  • 同步任務都在主線程上執(zhí)行涝缝,形成一個執(zhí)行棧
  • 主線程之外您觉,事件觸發(fā)線程管理著一個任務隊列,只要異步任務有了運行結果逆趋,就在任務隊列之中放置一個事件蒙揣。
  • 一旦執(zhí)行棧中的所有同步任務執(zhí)行完畢(此時JS引擎空閑),系統(tǒng)就會讀取任務隊列,將可運行的異步任務添加到可執(zhí)行棧中今瀑,開始執(zhí)行。

根據規(guī)范点把,事件循環(huán)是通過任務隊列的機制來進行協(xié)調的橘荠。一個 Event Loop 中,可以有一個或者多個任務隊列(task queue)郎逃,一個任務隊列便是一系列有序任務(task)的集合哥童;每個任務都有一個任務源(task source),源自同一個任務源的 task 必須放到同一個任務隊列褒翰,從不同源來的則被添加到不同隊列贮懈。 setTimeout/Promise 等API便是任務源,而進入任務隊列的是他們指定的具體執(zhí)行任務优训。

宏任務

(macro)task(又稱之為宏任務)朵你,可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放到執(zhí)行棧中執(zhí)行)。

瀏覽器為了能夠使得JS內部(macro)task與DOM任務能夠有序的執(zhí)行揣非,會在一個(macro)task執(zhí)行結束后抡医,在下一個(macro)task 執(zhí)行開始前,對頁面進行重新渲染早敬,流程如下:

(macro)task->渲染->(macro)task->...

(macro)task主要包含:script(整體代碼)忌傻、setTimeout、setInterval搁嗓、I/O仓洼、UI交互事件统刮、postMessage、MessageChannel、setImmediate(Node.js 環(huán)境)

微任務

microtask(又稱為微任務)艺沼,可以理解是在當前 task 執(zhí)行結束后立即執(zhí)行的任務够庙。也就是說伍伤,在當前task任務后乳蛾,下一個task之前,在渲染之前够委。

所以它的響應速度相比setTimeout(setTimeout是task)會更快荐类,因為無需等渲染。也就是說茁帽,在某一個macrotask執(zhí)行完后玉罐,就會將在它執(zhí)行期間產生的所有microtask都執(zhí)行完畢(在渲染前)屈嗤。

microtask主要包含:Promise.then、MutaionObserver吊输、process.nextTick(Node.js 環(huán)境)

運行機制

在事件循環(huán)中饶号,每進行一次循環(huán)操作稱為 tick,每一次 tick 的任務處理模型是比較復雜的季蚂,但關鍵步驟如下:

  • 執(zhí)行一個宏任務(棧中沒有就從事件隊列中獲让4)
  • 執(zhí)行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
  • 宏任務執(zhí)行完畢后扭屁,立即執(zhí)行當前微任務隊列中的所有微任務(依次執(zhí)行)
  • 當前宏任務執(zhí)行完畢算谈,開始檢查渲染,然后GUI線程接管渲染
  • 渲染完畢后料滥,JS線程繼續(xù)接管然眼,開始下一個宏任務(從事件隊列中獲取)

流程圖如下:

Promise和async中的立即執(zhí)行

我們知道Promise中的異步體現在thencatch中葵腹,所以寫在Promise中的代碼是被當做同步任務立即執(zhí)行的罪治。而在async/await中,在出現await出現之前礁蔗,其中的代碼也是立即執(zhí)行的。那么出現了await時候發(fā)生了什么呢雁社?

await做了什么

從字面意思上看await就是等待浴井,await 等待的是一個表達式,這個表達式的返回值可以是一個promise對象也可以是其他值霉撵。

很多人以為await會一直等待之后的表達式執(zhí)行完之后才會繼續(xù)執(zhí)行后面的代碼磺浙,實際上await是一個讓出線程的標志。await后面的表達式會先執(zhí)行一遍徒坡,將await后面的代碼加入到microtask中撕氧,然后就會跳出整個async函數來執(zhí)行后面的代碼。

這里感謝@chenjigeng的糾正:

由于因為async await 本身就是promise+generator的語法糖喇完。所以await后面的代碼是microtask伦泥。所以對于本題中的

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

等價于

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() ={
                console.log('async1 end');
        })
}

回到本題

//請寫出輸出內容
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}

console.log('script start');  // 1

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)中函數執(zhí)行順序的問題,其中包括async 锦溪,await不脯,setTimeoutPromise函數刻诊。下面來說一下本題中涉及到的知識點防楷。

以上就本道題涉及到的所有相關知識點了,下面我們再回到這道題來一步一步看看怎么回事兒则涯。

  1. 首先复局,事件循環(huán)從宏任務(macrotask)隊列開始冲簿,這個時候,宏任務隊列中亿昏,只有一個script(整體代碼)任務峦剔;當遇到任務源(task source)時,則會先分發(fā)任務到對應的任務隊列中去龙优。所以羊异,上面例子的第一步執(zhí)行如下圖所示:

    [圖片上傳失敗...(image-fd7cde-1584671574376)]

  2. 然后我們看到首先定義了兩個async函數,接著往下看彤断,然后遇到了 console 語句野舶,直接輸出 script start。輸出之后宰衙,script 任務繼續(xù)往下執(zhí)行平道,遇到 setTimeout,其作為一個宏任務源供炼,則會先將其任務分發(fā)到對應的隊列中:
    [圖片上傳失敗...(image-c2e621-1584671574376)]

  3. script 任務繼續(xù)往下執(zhí)行一屋,執(zhí)行了async1()函數,前面講過async函數中在await之前的代碼是立即執(zhí)行的袋哼,所以會立即輸出async1 start冀墨。
    遇到了await時,會將await后面的表達式執(zhí)行一遍涛贯,所以就緊接著輸出async2诽嘉,然后將await后面的代碼也就是console.log('async1 end')加入到microtask中的Promise隊列中,接著跳出async1函數來執(zhí)行后面的代碼弟翘。

    image

  4. script任務繼續(xù)往下執(zhí)行虫腋,遇到Promise實例。由于Promise中的函數是立即執(zhí)行的稀余,而后續(xù)的 .then 則會被分發(fā)到 microtask 的 Promise 隊列中去悦冀。所以會先輸出 promise1,然后執(zhí)行 resolve睛琳,將 promise2 分配到對應隊列盒蟆。

    image

  5. script任務繼續(xù)往下執(zhí)行,最后只有一句輸出了 script end师骗,至此茁影,全局任務就執(zhí)行完畢了。
    根據上述丧凤,每次執(zhí)行完一個宏任務之后募闲,會去檢查是否存在 Microtasks;如果有愿待,則執(zhí)行 Microtasks 直至清空 Microtask Queue浩螺。
    因而在script任務執(zhí)行完畢之后靴患,開始查找清空微任務隊列。此時要出,微任務中鸳君, Promise 隊列有的兩個任務async1 endpromise2,因此按先后順序輸出 async1 end患蹂,promise2或颊。當所有的 Microtasks 執(zhí)行完畢之后,表示第一輪的循環(huán)就結束了传于。

  6. 第二輪循環(huán)開始囱挑,這個時候就會跳回async1函數中執(zhí)行后面的代碼,然后遇到了同步任務 console 語句沼溜,直接輸出 async1 end平挑。這樣第二輪的循環(huán)就結束了。(也可以理解為被加入到script任務隊列中系草,所以會先與setTimeout隊列執(zhí)行)

  7. 第二輪循環(huán)依舊從宏任務隊列開始通熄。此時宏任務中只有一個 setTimeout,取出直接輸出即可找都,至此整個流程結束唇辨。

下面我會改變一下代碼來加深印象。

變式一

在第一個變式中我將async2中的函數也變成了Promise函數能耻,代碼如下:

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');

可以先自己看看輸出順序會是什么助泽,下面來公布結果:

script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout

在第一次macrotask執(zhí)行完之后,也就是輸出script end之后嚎京,會去清理所有microtask。所以會相繼輸出promise2隐解, async1 end 鞍帝,promise4,其余不再多說煞茫。

變式二

在第二個變式中帕涌,我將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');

可以先自己看看輸出順序會是什么续徽,下面來公布結果:

script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1

在輸出為promise2之后蚓曼,接下來會按照加入setTimeout隊列的順序來依次輸出,通過代碼我們可以看到加入順序為3 2 1钦扭,所以會按3纫版,2,1的順序來輸出客情。

變式三

變式三是我在一篇面經中看到的原題其弊,整體來說大同小異癞己,代碼如下:

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')

無非是在微任務那塊兒做點文章,前面的內容如果你都看懂了的話這道題一定沒問題的梭伐,結果如下:

script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout

參考文章

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末痹雅,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子糊识,更是在濱河造成了極大的恐慌绩社,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赂苗,死亡現場離奇詭異愉耙,居然都是意外死亡,警方通過查閱死者的電腦和手機哑梳,發(fā)現死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門劲阎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鸠真,你說我怎么就攤上這事悯仙。” “怎么了吠卷?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵锡垄,是天一觀的道長。 經常有香客問我祭隔,道長货岭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任疾渴,我火速辦了婚禮千贯,結果婚禮上,老公的妹妹穿的比我還像新娘搞坝。我一直安慰自己搔谴,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布桩撮。 她就那樣靜靜地躺著敦第,像睡著了一般。 火紅的嫁衣襯著肌膚如雪店量。 梳的紋絲不亂的頭發(fā)上芜果,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音融师,去河邊找鬼右钾。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的霹粥。 我是一名探鬼主播灭将,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼后控!你這毒婦竟也來了庙曙?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤浩淘,失蹤者是張志新(化名)和其女友劉穎捌朴,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體张抄,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡砂蔽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了署惯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片左驾。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖极谊,靈堂內的尸體忽然破棺而出诡右,到底是詐尸還是另有隱情,我是刑警寧澤轻猖,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布帆吻,位于F島的核電站,受9級特大地震影響咙边,放射性物質發(fā)生泄漏猜煮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一败许、第九天 我趴在偏房一處隱蔽的房頂上張望王带。 院中可真熱鬧,春花似錦市殷、人聲如沸愕撰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至绪妹,卻和暖如春甥桂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背邮旷。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工黄选, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓办陷,卻偏偏與公主長得像貌夕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子民镜,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容