學習JavaScript非同步、事件迴圈

Muse發表於2019-02-16

async 函式是 Generator 函式的語法糖。使用 關鍵字 async 來表示,在函式內部使用 await 來表示非同步。想較於 Generator,Async 函式的改進在於下面四點:

  • 內建執行器 Generator 函式的執行必須依靠執行器,而 Aysnc 函式自帶執行器,呼叫方式跟普通函式的呼叫一樣
  • 更好的語義 async 和 await 相較於 * 和 yield 更加語義化
  • 更廣的適用性 co 模組約定,yield 命令後面只能是Thunk 函式或 Promise物件。而 async 函式的 await 命令後面則可以是 Promise 或者原始型別的值(Number,string,boolean,但這時等同於同步操作)
  • 返回值是 Promise async 函式返回值是 Promise 物件,比 Generator 函式返回的 Iterator 物件方便,可以直接使用 then() 方法進行呼叫

await命令:正常情況下,await命令後面是一個 Promise 物件,返回該物件的結果。如果不是 Promise 物件,就直接返回對應的值

下面給大家看一道之前看過的題:

function test1() {
    console.log("執行test1");
    return "test1";
}

 function test2() {
    console.log("執行test2");
    return Promise.resolve("hello test2");
}

async function asyncTest() {
    console.log("asyncTest start...");
    const v1 = await test1();
    console.log(v1);
    const v2 = await test2();
    console.log(v2);
    console.log(v1, v2);
}

setTimeout(function(){
    console.log(`setTimeout`)
},0)  

asyncTest();


new Promise(function(resolve){
    console.log(`promise1`)
    resolve();
}).then(function(){
    console.log(`promise2`)
})
console.log(`test end`)

這道題結合了setTimeout、async、promise非同步函式,根據三種不同非同步任務執行順序可以學習js引擎的事件迴圈機制,我們們先看下結果:

test start...
執行test1
promise1
test end
test1
執行test2
promise2
hello test2
test1,hello test2
setTimeout

再講答案之前先理解以下幾個概念:

事件迴圈與訊息佇列

JS引擎執行緒遇到非同步(DOM事件監聽、網路請求、setTimeout計時器等…),會交給相應的執行緒單獨去維護非同步任務,等待某個時機(計時器結束、網路請求成功、使用者點選DOM),然後由 事件觸發執行緒 將非同步對應的 回撥函式 加入到訊息佇列中,訊息佇列中的回撥函式等待被執行。

同時,JS引擎執行緒會維護一個 執行棧,同步程式碼會依次加入執行棧然後執行,結束會退出執行棧。

如果執行棧裡的任務執行完成,即執行棧為空的時候(即JS引擎執行緒空閒),事件觸發執行緒才會從訊息佇列取出一個任務(即非同步的回撥函式)放入執行棧中執行。

訊息佇列是類似佇列的資料結構,遵循**先入先出(FIFO)**的規則。

執行完了後,執行棧再次為空,事件觸發執行緒會重複上一步操作,再取出一個訊息佇列中的任務,這種機制就被稱為事件迴圈(event loop)機制。

主程式碼塊(script)依次加入執行棧,依次執行,主程式碼塊為:

  • setTimeout()
  • asyncTest()
  • Promise()
  • console.log(`test end`)

巨集任務與微任務

macrotask(巨集任務) :主程式碼塊、setTimeout、setInterval等(可以看到,事件佇列中的每一個事件都是一個 macrotask,現在稱之為巨集任務佇列

和 microtask(微任務):Promise、process.nextTick等

JS引擎執行緒首先執行主程式碼塊。
每次執行棧執行的程式碼就是一個巨集任務,包括任務佇列(巨集任務佇列)中的,因為執行棧中的巨集任務執行完會去取任務佇列(巨集任務佇列)中的任務加入執行棧中,即同樣是事件迴圈的機制。
在執行巨集任務時遇到Promise等,會建立微任務(.then()裡面的回撥),並加入到微任務佇列隊尾。
microtask必然是在某個巨集任務執行的時候建立的,而在下一個巨集任務開始之前,瀏覽器會對頁面重新渲染(task >> 渲染 >> 下一個task(從任務佇列中取一個))。同時,在上一個巨集任務執行完成後,渲染頁面之前,會執行當前微任務佇列中的所有微任務。
也就是說,在某一個macrotask執行完後,在重新渲染與開始下一個巨集任務之前,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)。

執行機制:

  1. 執行一個巨集任務(棧中沒有就從事件佇列中獲取)
  2. 執行過程中如果遇到微任務,就將它新增到微任務的任務佇列中
  3. 巨集任務執行完畢後,立即執行當前微任務佇列中的所有微任務(依次執行)
  4. 當前巨集任務執行完畢,開始檢查渲染,然後GUI執行緒接管渲染
  5. 渲染完畢後,JS引擎執行緒繼續,開始下一個巨集任務(從巨集任務佇列中獲取)

遇到非同步函式 setTimeout,交給定時器觸發執行緒 setTimeout加入巨集任務佇列,JS引擎執行緒繼續,出棧;

執行非同步函式asyncTest,首先列印test start…

執行await test1函式首先列印”執行test1″,await讓出執行緒去執行後面的程式碼;

執行Promise 首先列印promise1,then後面函式為微任務,新增到微任務佇列中

JS引擎執行緒繼續向下執行同步程式碼console.log(`test end`)列印`test end`

回到asyncTest執行await test1由於返回不是promise物件,所以直接返回test1

執行await test2()同樣先列印 “執行test2″,由於test2返回promise物件 會加入到之前微任務佇列中,await繼續讓出

執行微任務佇列,由於任務佇列遵循先進先出結果,所以首先列印promise2,然後列印hello test2

微任務佇列執行完成後繼續執行asyncTest內 await之後的程式碼列印 倆個await返回的值 –test1,hello test2

最後回到巨集任務佇列執行setTimeout,列印setTimeout

如果我把test1變成非同步函式,大家再思考一下會列印什麼結果:

async  function test1() {
    console.log("執行test1");
    return "test1";
}

 function test2() {
    console.log("執行test2");
    return Promise.resolve("hello test2");
}

async function asyncTest() {
    console.log("asyncTest start...");
    const v1 = await test1();
    console.log(v1);
    const v2 = await test2();
    console.log(v2);
    console.log(v1, v2);
}

setTimeout(function(){
    console.log(`setTimeout`)
},0)  

asyncTest();


new Promise(function(resolve){
    console.log(`promise1`)
    resolve();
}).then(function(){
    console.log(`promise2`)
})
console.log(`test end`)

以上就是此程式碼執行過程,由於本人也是在學習總結中,如有不對的地方請指教,共同學習,一起進步!!!

相關文章