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都執行完畢(在渲染前)。
執行機制:
- 執行一個巨集任務(棧中沒有就從事件佇列中獲取)
- 執行過程中如果遇到微任務,就將它新增到微任務的任務佇列中
- 巨集任務執行完畢後,立即執行當前微任務佇列中的所有微任務(依次執行)
- 當前巨集任務執行完畢,開始檢查渲染,然後GUI執行緒接管渲染
- 渲染完畢後,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`)
以上就是此程式碼執行過程,由於本人也是在學習總結中,如有不對的地方請指教,共同學習,一起進步!!!