前端高級面試題-完全掌握JS事件循環原理

前言

爲何寫這篇文章?前端

由於這道題涉及了不少知識點:同步任務、異步任務、宏任務、微任務、任務隊列、執行棧、js運行機制、EventLoop,因此想整理一下,寫一篇文章,但願對小夥伴們有所幫助!node

經典題

下面這段promise、async和await代碼,請問控制檯打印順序?面試

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');
複製代碼

正確答案promise

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
複製代碼

注:由於是一道前端面試題,因此答案是以瀏覽器的eventloop機制爲準的,在node平臺上運行會有差別瀏覽器

相信大多數前端都知道,這道題考的就是js裏面的事件循環和任務隊列markdown

知識點

同步任務、異步任務、宏任務、微任務、任務隊列、執行棧、js運行機制、Event Loop異步

這題考察的是js中的事件循環和任務隊列,注意如下幾點:async

  • Promise優先於setTimeout宏任務。因此,setTimeout回調會在最後執行
  • Promise一旦被定義,就會當即執行
  • Promise的reject和resolve是異步執行的回調。因此,resolve()會被放到回調隊列中,在主函數執行完和setTimeout前調用
  • await執行完後,會讓出線程。async標記的函數會返回一個Promise對象

async介紹

  • async function 聲明將定義一個返回 AsyncFunction 對象的異步函數
  • 當調用一個 async 函數時,會返回一個 Promise 對象
  • 當這個 async 函數返回一個值時,Promise 的 resolve 方法會負責傳遞這個值
  • 當 async 函數拋出異常時,Promise 的 reject 方法也會傳遞這個異常值

它經過返回一個 Promise 對象來返回結果最大的特色是:函數

經過 async / await 將異步的操做,但寫法和結構倒是和咱們平時寫的(同步代碼)是同樣

await

  • await操做符用於等待一個Promise對象
  • await表達式會暫停當前async
  • function的執行,等待Promise處理完成,若Promise正常處理,其回調的resolve函數參數做爲await表達式的值,繼續執行async function
  • await意味讓出線程操做

返回值(return_value):返回 Promise 對象的處理結果。若是等待的不是 Promise 對象,則返回該值自己,oop

因此,當await操做符後面的表達式是一個Promise的時候,它的返回值,實際上就是Promise的回調函數resolve的參數

咱們都知道Promise是一個當即執行函數,可是他的成功(或失敗:reject)的回調函數resolve倒是一個異步執行的回調。當執行到resolve()時,這個任務會被放入到回調隊列中,等待調用棧有空閒時事件循環再來取走它

promise

async會返回Promise對象,若是返回值不是Promise對象則調用Promise resolve來換成Promise對象

async/await創建在Prmise機制上

總結一句話:帶async關鍵字的函數,它使得你的函數的返回值一定是 promise 對象上

EvenLoop

這裏先簡單的說一些Event Loop的概念,在最近這段時間我會寫一篇關於Event Loop的文章

Javascript是單線程的,全部的同步任務都會在主線程中執行

主線程以外,還有一個任務隊列。每當一個異步任務有結果了,就往任務隊列裏塞一個事件。 當主線程中的任務,都執行完以後,系統會 「依次」 讀取任務隊列裏的事件。與之相對應的異步任務進入主線程,開始執行

異步任務之間,會存在差別,因此它們執行的優先級也會有區別。大體分爲 微任務(micro task,如:Promise、MutaionObserver等)和宏任務(macro task,如:setTimeout、setInterval、I/O等)。同一次事件循環中,微任務永遠在宏任務以前執行

主線程會不斷重複上面的步驟,直到執行完全部任務

題目結果分析過程

第一步,輸出script start

雖然有兩個函數聲明,有async關鍵字,可是沒有調用咱們就不看,直接打印同步代碼console.log(‘script start’)

第二步,輸出async1 start

在執行async1這個函數的時候,async表達式定義的函數也是當即執行 在前面咱們說過看到帶有async關鍵字函數,不用慌,它僅僅是把return值包裝成了promise,因此就很普通的打印 console.log( 'async1 start' )

第三步,輸出async2

async2是async定義的函數,輸出async2並返回promise對象, await後,中斷async函數,先執行async外的同步代碼, 目前就直接打印 console.log('async2')

第四步,輸出promise1

執行new Promise(),Promise構造函數是直接調用的同步代碼,因此就打印console.log( 'promise1' )

第五步,輸出script end

由於上一步先打印了promise1,而後執行到resolve的時候,而後跳出promise繼續向下執行,因此就打印console.log( 'script end' )

第六步,輸出async1 end

由於await定義的這個promise已經執行完,而且返回結果,因此繼續執行async1函數後的任務,就是console.log(‘async1 end’)

第七步,輸出promise2

由於前面的new promise放進resolve回調,這個resolve被放到調用棧執行,因此就打印console.log('promise')

第八步,輸出setTimerout

最後執行定時器(宏任務)setTimeout,打印console.log( 'setTimerout' )

題目變式一

在這個變式中我將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
複製代碼

題目變式二

我將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
複製代碼

題目變式三

這道題總體來講與上面題大同小異與,代碼以下:

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
複製代碼

最後

若是以爲本文還不錯,記得點個贊哦!

歡迎你們加入,一塊兒學習前端,共同進步!

cmd-markdown-logo

cmd-markdown-logo

相關文章
相關標籤/搜索