原文出自:https://www.pandashen.com
前言
這篇文章是異步發展流程系列的最後一篇,可能會涉及 Promise、Generators、co 等前置知識,如果對這些不是很瞭解可以看這個系列的前三篇:
如果已經具備這些前置知識,那我們繼續看看今天的主角,JavaScript 異步編程的終極大招 async/await
。
async/await 簡介
async/await
指的是兩個關鍵字,是 ES7 引入的新標準,async
關鍵字用於聲明 async
函數,await
關鍵字用來等待異步(必須是 Promise)操作,說白了 async/await
就是 Generators + co 的語法糖。
async/await
和 Generators + co 的寫法非常的相似,只是把用於聲明 Generator 函數的 *
關鍵字替換成了 async
並寫在了 function
關鍵字的前面,把 yield
關鍵字替換成了 await
;另外,async
函數是基於 Promise 的,await
關鍵字後面等待的異步操作必須是一個 Promise 實例,當然也可以是原始類型的值,只不過這時的執行效果等同於同步,與 Generator 不同的是,await
關鍵字前可以使用變量去接收這個正在等待的 Promise 實例執行後的結果。
async 函數的基本用法
async
函數返回一個 Promise 實例,可以使用 then
方法添加回調函數。當函數執行的時候,只要遇到 await
就會等待,直到 await
後面的同步或異步操作完成,再接着執行函數體內後面的語句。
1、async 函數聲明
async
的聲明方式大概有以下幾種:
// async 函數聲明
// 函數聲明
async function fn() {}
// 函數表達式
const fn = async function() {};
// 箭頭函數
const fn = async () => {};
// 作爲對象的方法
let obj = {
async fn() {}
};
// 作爲 class 的方法
class Person(name) {
constructor () {
this.name = name;
}
async getName() {
const name = await this.name;
return name;
}
}
在上一篇介紹 Generators + co 的文章中我們舉了一個例子,使用 NodeJS 的 fs
模塊連續異步讀文件,第一個文件名爲 a.txt
,讀到的內容爲 b.txt
,作爲要讀的第二個文件的文件名,繼續讀 b.txt
後將讀到的內容 “Hello world” 打印出來。
我們來使用 async/await
的方式來實現一下:
// async 函數實現文件讀取
// 引入依賴
const fs = require("fs");
const util = require("util");
// 將 fs.readFile 轉換成 Promise
const readFile = util.promisify(fs.readFile);
// 聲明 async 函數
async function read(file) {
let aData = await readFile(file, "utf8");
let bData = await readFile(aData, "utf8");
return bData;
}
// 調用 async 函數
read("a.txt").then(data => {
console.log(data); // Hello world
});
其實對比上一篇文章 Generator 的案例,與 Generator 函數一樣,寫法像同步,執行是異步,不同的是我們即沒有手動調用 next
方法,也沒有藉助 co
庫,其實是 async
函數內部集成了類似於 co
的執行器,幫我們在異步完成後自動向下執行代碼,所以說 async/await
是 Generators + co 的語法糖。
2、async 函數錯誤處理
async
函數內部如果執行錯誤可以有三種方式進行錯誤處理:
- 在
await
後面的 Promise 實例使用then
方法錯誤的回調或catch
方法進行錯誤處理; - 如果有多個
await
,可以在async
函數執行完後使用catch
方法統一處理; - 由於
async
內部代碼是同步的寫法,多個await
的情況也可以使用try...catch...
進行處理。
需要注意的是,如果在 async
函數內部使用了 try...catch...
又在函數執行完後使用了 catch
,錯誤會優先被同步的 try...catch...
捕獲到,後面的 catch
就不會再捕獲了。
// async 函數異常捕獲
// 第一種
async function fn() {
let result = await Promise.reject("error").catch(err => {
console.log(err);
});
}
fn(); // error
// 第二種
async function fn() {
try {
let val1 = await Promise.reject("error");
let val2 = await Promise.resolve("success");
} catch (e) {
console.log(e);
}
}
fn(); // error
// 第三種
async function fn() {
let val1 = await Promise.resolve("success");
let val2 = await Promise.reject("error");
}
fn().catch((err => console.log(err))); // error
3、await 異步併發
在 async
函數中,如果有多個 await
互不依賴,這種情況下如果執行一個,等待一個完成,再執行一個,再等待完成,這樣是很浪費性能的,所以我們要把這些異步操作同時觸發。
假設我們異步讀取兩個文件,且這兩個文件不相關,我可以使用下面的方式來實現:
// await 異步併發
// 前置
const fs = require("fs");
const util = require("util");
const readFile = util.promisify(fs.readFile);
// 需要改進的 async 函數
async function fn() {
let aData = await readFile("a.txt", "utf8");
let bData = await readFile("b.txt", "utf8");
return [aData, bData];
}
fn();
// 在 async 函數外部觸發異步
let aDataPromise = readFile("a.txt", "utf8");
let bDataPromise = readFile("b.txt", "utf8");
async function fn() {
let aData = await aDataPromise;
let bData = await bDataPromise;
return [aData, bData];
}
fn();
// 使用 Promise.all
async function fn() {
let dataArr = await Promise.all(
readFile("a.txt", "utf8"),
readFile("a.txt", "utf8")
);
return dataArr;
}
fn();
4、使用 async/await 的注意點
使用 async/await
應注意以下幾點:
- 對
await
習慣性錯誤處理; -
await
命令後互不依賴的異步應同時觸發; -
async
函數中,函數的執行上/下文發生變化時,不能使用await
(如使用forEach
循環的回調中)。
針對第一點,在 async
函數中 await
命令後面大多情況下是 Promise 異步操作,運行結果可能出現錯誤並調用 reject
函數,最好對這個 await
語句進行錯誤處理,具體方式參照 async
函數基本用法中關於錯誤處理的內容。
針對第二點,如果兩個或多個 await
命令後的異步操作沒有依賴關係,執行時,需先觸發第一個,等待異步完成,再觸發第二個,再等異步完成,依次類推,這樣比較耗時,性能不好,所以應該將這些異步操作同時觸發,觸發方式參照 async
函數基本用法中的 await
異步併發的內容。
針對第三點,如果聲明一個 async
函數並傳入一個數組,數組裏面存儲的都是 Promise 實例,若使用 forEach
循環數組,由於函數的執行上/下文發生了變化,此時使用 await
命令會報錯。
// 循環內使用 await
// 創建 Promise 實例
let p1 = Promise.resolve("p1 success");
let p2 = Promise.resolve("p2 success");
let p3 = Promise.resolve("p3 success");
// async 函數
async function fn(promises) {
promise.forEach(function (promise) {
await promise;
});
}
fn([p1, p2, p3]); // 執行時報錯
// 修改方式
async function fn(promises) {
for(let i = 0; i < promises.length; i++) {
await pormises[i];
}
}
fn([p1, p2, p3]); // 正常執行
<hr/>
總結
async/await
的實現原理,其實就是在 async
函數內部邏輯映射成了 Generator 函數並集成了一個類似於 co
的執行器,所以我們使用 async/await
的時候,代碼更簡潔,沒有了自己觸發遍歷器的 next
或調用 co
充當執行器的過程,只需要關心 async
函數的內部邏輯就可以了,因爲寫法與同步相同,更提高了代碼的可讀性,所以說 async/await
是異步編程的終極大招。
由於 async/await
是 ES7 規範,在瀏覽器端的支持並不是那麼的友好,所以現在這種寫法多用在 NodeJS 的異步操作當中,在 NodeJS 框架 Koa 2.x
版本得到廣泛應用。
最後希望大家在讀過異步發展流程這個系列之後,對 JavaScript 異步已經有了較深的認識,並可以在不同情況下遊刃有餘的使用這些處理異步的編程手段。