JavaScript 的事件循環(huán)

一道有關(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í)行順序的問題秀仲,其中包括asyncawait球凰,setTimeout阀蒂,Promise函數(shù)该窗。主要涉及到以下知識(shí)點(diǎn)。

任務(wù)隊(duì)列

  • JS分為同步任務(wù)和異步任務(wù)
  • 同步任務(wù)都在主線程上執(zhí)行蚤霞,形成一個(gè)執(zhí)行棧
  • 主線程之外,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列(包括 宏任務(wù)隊(duì)列微任務(wù)隊(duì)列)义钉,只要異步任務(wù)有了運(yùn)行結(jié)果昧绣,就在任務(wù)隊(duì)列之中放置一個(gè)事件。
  • 一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時(shí)JS引擎空閑)捶闸,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列夜畴,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中,開始執(zhí)行


宏任務(wù)

task(又稱之為宏任務(wù))删壮,可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)贪绘。

瀏覽器為了能夠使得JS內(nèi)部task與DOM任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè)task執(zhí)行結(jié)束后央碟,在下一個(gè)(macro)task 執(zhí)行開始前税灌,對(duì)頁(yè)面進(jìn)行重新渲染,流程如下:

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(wú)需等渲染。也就是說淳附,在某一個(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ì)列中獲取)

流程圖如下:

image

簡(jiǎn)單的說就是按照程序順序執(zhí)行道偷,遇到微任務(wù)就放到放到微任務(wù)隊(duì)列缀旁,遇到宏任務(wù)就放到宏任務(wù)隊(duì)列,執(zhí)行棧執(zhí)行完之后勺鸦,再清空微任務(wù)隊(duì)列并巍,接著執(zhí)行宏任務(wù)隊(duì)列

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

我們知道Promise中的異步體現(xiàn)在thencatch中,所以寫在Promise中的代碼是被當(dāng)做同步任務(wù)立即執(zhí)行的换途。而在async/await中懊渡,在出現(xiàn)await出現(xiàn)之前,其中的代碼也是立即執(zhí)行的怀跛。那么出現(xiàn)了await時(shí)候發(fā)生了什么呢距贷?

await做了什么

很多人以為await會(huì)一直等待之后的表達(dá)式執(zhí)行完之后才會(huì)繼續(xù)執(zhí)行后面的代碼,實(shí)際上await是一個(gè)讓出線程的標(biāo)志吻谋。await后面的表達(dá)式會(huì)先執(zhí)行一遍忠蝗,將await后面的代碼加入到microtask中,然后就會(huì)跳出整個(gè)async函數(shù)來執(zhí)行后面的代碼漓拾。

由于因?yàn)閍sync await 本身就是promise+generator的語(yǔ)法糖阁最。所以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');
        })
}

看一個(gè)使用 await 的例子:

let a = 0
let b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1

對(duì)于以上代碼你可能會(huì)有疑惑骇两,讓我來解釋下原因

  • 首先函數(shù) b 先執(zhí)行速种,在執(zhí)行到 await 10 之前變量 a 還是 0,因?yàn)?await 內(nèi)部實(shí)現(xiàn)了 generator 低千,generator 會(huì)保留堆棧中東西配阵,所以這時(shí)候 a = 0 被保存了下來
  • 因?yàn)?await 是異步操作,后來的表達(dá)式不返回 Promise 的話示血,就會(huì)包裝成 Promise.reslove(返回值)棋傍,然后會(huì)去執(zhí)行函數(shù)外的同步代碼
  • 同步代碼執(zhí)行完畢后開始執(zhí)行異步代碼,將保存下來的值拿出來使用难审,這時(shí)候 a = 0 + 10

下面的題目有助于加深對(duì)于javascript事件循環(huán)的理解

變式一

在第一個(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

變式二

在第二個(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

變式三

變式三是我在一篇面經(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ú)非是在微任務(wù)那塊兒做點(diǎn)文章蒂萎,前面的內(nèi)容如果你都看懂了的話這道題一定沒問題的,結(jié)果如下:

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

參考文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末淮椰,一起剝皮案震驚了整個(gè)濱河市岖是,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌实苞,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烈疚,死亡現(xiàn)場(chǎng)離奇詭異黔牵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)爷肝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門猾浦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人灯抛,你說我怎么就攤上這事金赦。” “怎么了对嚼?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵夹抗,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我纵竖,道長(zhǎng)漠烧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任靡砌,我火速辦了婚禮已脓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘通殃。我一直安慰自己度液,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布画舌。 她就那樣靜靜地躺著堕担,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骗炉。 梳的紋絲不亂的頭發(fā)上照宝,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音句葵,去河邊找鬼厕鹃。 笑死兢仰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的剂碴。 我是一名探鬼主播把将,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼忆矛!你這毒婦竟也來了察蹲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤催训,失蹤者是張志新(化名)和其女友劉穎洽议,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漫拭,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡亚兄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了采驻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片审胚。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖礼旅,靈堂內(nèi)的尸體忽然破棺而出膳叨,到底是詐尸還是另有隱情,我是刑警寧澤痘系,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布菲嘴,位于F島的核電站,受9級(jí)特大地震影響碎浇,放射性物質(zhì)發(fā)生泄漏临谱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一奴璃、第九天 我趴在偏房一處隱蔽的房頂上張望悉默。 院中可真熱鬧,春花似錦苟穆、人聲如沸抄课。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)跟磨。三九已至,卻和暖如春攒盈,著一層夾襖步出監(jiān)牢的瞬間抵拘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工型豁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留僵蛛,地道東北人尚蝌。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像充尉,于是被迫代替她去往敵國(guó)和親飘言。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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

  • JS為什么是單線程的? 最初設(shè)計(jì)JS是用來在瀏覽器驗(yàn)證表單操控DOM元素的是一門腳本語(yǔ)言驼侠,如果js是多線程的那么兩...
    船長(zhǎng)___閱讀 2,186評(píng)論 1 15
  • 事件循環(huán)(evenloop) 事件循環(huán)機(jī)制是宿主環(huán)境提供的姿鸿。js中處理異步,增加了任務(wù)隊(duì)列的概念(你不知道的js中...
    209bd3bc6844閱讀 2,811評(píng)論 0 1
  • 弄懂js異步 講異步之前倒源,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop苛预。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,706評(píng)論 0 5
  • 每個(gè)人都會(huì)對(duì)自己離開這個(gè)人世后的去處充滿好奇,但從來沒有也不會(huì)有人知道會(huì)是怎樣笋熬,所以碟渺,從古至今,各種宗教突诬,各...
    丫頭利貞閱讀 454評(píng)論 0 1
  • 創(chuàng)業(yè),創(chuàng)業(yè)的定義是什么芜繁?你覺得什么是創(chuàng)業(yè)旺隙?你覺得創(chuàng)業(yè)應(yīng)該怎么做?你覺得創(chuàng)業(yè)應(yīng)該報(bào)有什么樣的心態(tài)骏令? ...
    沈小彧閱讀 484評(píng)論 1 2