學習JavaScript異步、事件循環

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')

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章