javascript事件循環機制及面試題詳解

javascript是單線程執行的程序,也就是它只有一條主線,所有的程序都是逐行“排隊”執行,在這種情況下可能存在一些問題,比如說setTimeout、ajax等待執行的時間較長,就會阻塞后續代碼的執行,使得整個程序執行的耗時非常久,那么為了應對這樣一個問題,javascript代碼在執行的時候,是有幾個“通道”的。

首先是調用棧,執行耗時較短的操作,耗時較長的操作先放置到任務隊列中,任務隊列又分為宏任務(macro-task)和微任務(micro-task),微任務中隊列中放置的是 promise.then、aysnc、await 這樣操作,宏任務隊列中放置的是 setTimeout、ajax、onClick事件,等調用棧的任務執行完成再輪詢微任務隊列,微任務隊列中任務執行完成之后再執行宏任務。

這里提到了棧和隊列,簡單說一下這兩種數據結構,棧是一種后進先出的結構,只能從尾部進入,從尾部刪除,拿生活中的場景來打比方,就好像自助餐的餐盤,最先放的盤子在最底下,最后放的盤子在最上面,需要把最上面的盤子一個個拿走,才能拿到最下面的盤子。

而隊列,是一種先進先出的結構,從尾部進入,從頭部刪除,就像我們去排隊買東西,先去的同學可以先買到。

image.png

再回到事件循環機制(event loop),不阻塞主進程的程序放入調用棧中,壓入棧底,執行完了就會彈出,如果是函數,那么執行完函數里所有的內容才會彈出,而阻塞主進程的程序放入任務隊列中,他們需要“排隊”依次執行。

首先來個簡單的例子,判斷以下程序的執行順序

new Promise(resolve => {
  console.log('promise');
  resolve(5);
}).then(value=>{
  console.log('then回調', value)
})

function func1() {
  console.log('func1');
}

setTimeout(() => {
  console.log('setTimeout');
});

func1();

創建一個promise的實例就是開啟了一個異步任務,傳入的回調函數,也叫做excutor 函數,會立刻執行,所以輸入promise,使用resolve返回一個成功的執行結果,then函數里的執行會推入到微任務隊列中等待調用棧執行完成才依次執行。

向下執行發現定義了一個func1的函數,函數此時沒有被調用,則不會推入調用棧中執行。程序繼續往下,發現調用setTimeout函數,將打印的setTimeout推入宏任務隊列,再往下執行調用函數func1,將func1推入調用棧中,執行func1函數,此時輸出fun1。

調用棧里所有的內容都執行完成,開始輪詢微任務隊列,輸入then回調5,最后執行宏任務隊列,輸入setTimeout

image.png

再來看一個復雜的例子

setTimeout(function () {
  console.log("set1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});

new Promise(function (resolve) {
  console.log("pr1");
  resolve();
}).then(function () {
  console.log("then1");
});

setTimeout(function () {
  console.log("set2");
});

console.log(2);

queueMicrotask(() => {
  console.log("queueMicrotask1")
});

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});

setTimeout執行的回調函數("set1")直接被放置到宏任務隊列中等待,Promise的excutor函數立刻執行,首先輸入 pr1,Promise.then函數("then1")放入微任務隊列中等待,下面的setTimeout執行的回調函數("set2")也被放置到宏任務隊列中,排在("set1")后面,接下來調用棧中輸出2,queueMicrotask表示開啟一個微任務,與Promise.then函數效果一致,("queueMicrotask1")放入微任務隊列中,再往下執行,new Promise的excutor函數立刻執行,then函數("then3")放到微任務隊列中等待,此時調用棧出已依次輸入pr1,2。

調用棧中程序已執行完,來到微任務隊列中執行微任務,依次輸出then1,queueMicrotask1,then3。

此時微任務隊列中的任務也執行完成,來到宏任務隊列中,輸出set1,執行Promise的excutor函數,resolve即返回成功的執行結果,then函數("then2")放入微任務中,一旦微任務隊列中有任務,就不會往后執行宏任務,所以宏任務隊列中的另一個setTimeout函數("set2")此時不會執行,來到微任務隊列中執行("then2"),輸出then2,再執行一個promise函數,("then4")被放入到微任務隊列中,輸出then4。

微任務隊列也都執行完成,此時來到宏任務隊列中,執行set2。

所以最后的輸出結果為:

pr1
2
then1
queueMicrotask1
then3
set1
then2
then4
set2

簡單圖示如下

image.png

最后一道題,加上了 async、await

先來一個結論,通過async定義的函數在調用棧中執行,await 將異步程序變成同步,所以await后面執行的程序需要等到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')

函數只有調用的時候才會推入調用棧中,所以最先執行的是 console.log,即輸出 script start,然后setTimeout函數("setTimeout")放入宏任務隊列中等待,調用async1函數,輸出 async1 start,執行async2函數,輸出async2,("async1 end")放入微任務隊列中等待,繼續向下執行Promise函數,輸出 promise1,then函數中的("promise2")放入微任務隊列中等待,輸出 script end。

調用棧的程序都已執行完畢,此時開始執行微任務隊列中的程序,依次輸出 async1 end、promise2。

微任務隊列中的程序也已執行完成,開始執行宏任務中的程序,輸出setTimeout。

輸出順序為

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

簡單圖示如下

image.png

判斷執行順序可以記住以下幾個重點

1、promise中的回調函數立刻執行,then中的回調函數會推入微任務隊列中,等待調用棧所有任務執行完才執行

2、async函數里的內容是放入調用棧執行的,await的下一行內容是放入微任務執行的

3、調用棧執行完成后,會不斷的輪詢微任務隊列,即使先將宏任務推入隊列,也會先執行微任務

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容