首頁技術(shù)文章正文

從async/await面試題看宏觀任務(wù)和微觀任務(wù)

更新時間:2020-11-10 來源:黑馬程序員 瀏覽量:

先來看這樣一道面試題:
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(() => {
  console.log('setTimeout')
}, 0);
async1()
new Promise(resolve => {
    console.log('promise1')
    resolve()
  })
  .then(() => {
    console.log('promise2')
  })
console.log('script end')


1605003946016_res1.png


這道題主要考察的是事件循環(huán)中函數(shù)執(zhí)行順序的問題,其中包括async ,await,setTimeout,Promise函數(shù)。下面來說一下本題中涉及到的知識點(diǎn)。

任務(wù)隊列

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

·JS分為同步任務(wù)和異步任務(wù)

·同步任務(wù)都在主線程上執(zhí)行,形成一個執(zhí)行棧

·主線程之外,事件觸發(fā)線程管理著一個任務(wù)隊列,只要異步任務(wù)有了運(yùn)行結(jié)果,就在任務(wù)隊列之中放置一個事件。

·一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時JS引擎空閑),系統(tǒng)就會讀取任務(wù)隊列,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中,開始執(zhí)行。

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

1605003987277_隊列.png


宏任務(wù)

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

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

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


(macro)task主要包含:

1.script(整體代碼)

2.setTimeout

3.setInterval

4.I/O

5.UI交互事件

6.postMessage

7.MessageChannel

8.setImmediate(Node.js 環(huán)境)

 

微任務(wù)

microtask(又稱為微任務(wù)),可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)。也就是說,在當(dāng)前task任務(wù)后,下一個task之前,在渲染之前。

所以它的響應(yīng)速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染。也就是說,在某一個macrotask執(zhí)行完后,就會將在它執(zhí)行期間產(chǎn)生的所有microtask 都 執(zhí)行完畢(在渲染前)。

microtask主要包含:

1. Promise.then

2. MutaionObserver

3. process.nextTick(Node.js 環(huán)境)


運(yùn)行機(jī)制

在事件循環(huán)中,每進(jìn)行一次循環(huán)操作稱為 tick,每一次 tick 的任務(wù)處理模型是比較復(fù)雜的,但關(guān)鍵步驟如下:


·執(zhí)行一個宏任務(wù)(棧中沒有就從事件隊列中獲取)

·執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊列中

·宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊列中的**所有微任務(wù)**(依次執(zhí)行)

·當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染

·渲染完畢后,JS線程繼續(xù)接管,開始下一個宏任務(wù)(從事件隊列中獲取)


流程圖如下:

1605004027341_流程.png


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

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

await做了什么

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

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

由于因為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')
  })
}

以上就本道題涉及到的所有相關(guān)知識點(diǎn)了,下面我們再回到這道題來一步一步看看怎么回事兒。

  1. 首先,事件循環(huán)從宏任務(wù)(macrotask)隊列開始,這個時候,宏任務(wù)隊列中,只有一個script(整體代碼)任務(wù);當(dāng)遇到任務(wù)源(task source)時,則會先分發(fā)任務(wù)到對應(yīng)的任務(wù)隊列中去。所以,上面例子的第一步執(zhí)行如下圖所示:



    1605004063642_1.png

  2. 然后我們看到首先定義了兩個async函數(shù),接著往下看,然后遇到了 console 語句,直接輸出 script start。輸出之后,script 任務(wù)繼續(xù)往下執(zhí)行,遇到 setTimeout,其作為一個宏任務(wù)源,則會先將其任務(wù)分發(fā)到對應(yīng)的隊列中:


    1605004117246_2.png

  3. script 任務(wù)繼續(xù)往下執(zhí)行,執(zhí)行了async1()函數(shù),前面講過async函數(shù)中在await之前的代碼是立即執(zhí)行的,所以會立即輸出async1 start。

    遇到了await時,會將await后面的表達(dá)式執(zhí)行一遍,所以就緊接著輸出async2,然后將await后面的代碼也就是console.log('async1 end')加入到microtask中的Promise隊列中,接著跳出async1函數(shù)來執(zhí)行后面的代碼。


    1605004171205_3.png

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


    1605004307649_4.png

  5. script任務(wù)繼續(xù)往下執(zhí)行,最后只有一句輸出了 script end,至此,全局任務(wù)就執(zhí)行完畢了。

    根據(jù)上述,每次執(zhí)行完一個宏任務(wù)之后,會去檢查是否存在 Microtasks;如果有,則執(zhí)行 Microtasks 直至清空 Microtask Queue。

    因而在script任務(wù)執(zhí)行完畢之后,開始查找清空微任務(wù)隊列。此時,微任務(wù)中, Promise 隊列有的兩個任務(wù)async1 end和promise2,因此按先后順序輸出 async1 end,promise2。當(dāng)所有的 Microtasks 執(zhí)行完畢之后,表示第一輪的循環(huán)就結(jié)束了。

  6. 第二輪循環(huán)依舊從宏任務(wù)隊列開始。此時宏任務(wù)中只有一個 setTimeout,取出直接輸出即可,至此整個流程結(jié)束。

變式一

在第一個變式中我將async2中的函數(shù)也變成了Promise函數(shù),代碼如下:

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  new Promise(resolve => {
      console.log('promise1')
      resolve()
    })
    .then(() => {
      console.log('promise2')
    })
}
console.log('script start')
setTimeout(() => {
  console.log('setTimeout')
}, 0);
async1()
new Promise(resolve => {
    console.log('promise3')
    resolve()
  })
  .then(() => {
    console.log('promise4')
  })
console.log('script end')


運(yùn)行結(jié)果入下:

1605004336136_res2.png

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

變式二

在第二個變式中,我將async1中await后面的代碼和async2的代碼都改為異步的,代碼如下:

async function async1() {
  console.log('async1 start')
  await async2()
  setTimeout(() => {
    console.log('setTimeout1')
  }, 0)
}
async function async2() {
  setTimeout(() => {
    console.log('setTimeout2')
  }, 0)
}
console.log('script start')
setTimeout(() => {
  console.log('setTimeout3')
}, 0);
async1()
new Promise(resolve => {
    console.log('promise1')
    resolve()
  })
  .then(() => {
    console.log('promise2')
  })
console.log('script end')

執(zhí)行結(jié)果如下:

1605004360271_res3.png

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

只要前面的原理看懂了,任何的變式題都不會有問題。


猜你喜歡:

前端培訓(xùn)課程

jQuery怎樣使用選擇器獲取元素?

什么是變量?JavaScript變量命名規(guī)范介紹

分享到:
在線咨詢 我要報名
和我們在線交談!