掌握 Node.js 中的 async/await
你會在本文中學到如何使用 async 函數(async/await) 來簡化回調,以及基于 Promise 的 Node.js 應用。
異步語言結構已經在其它語言中存在好一陣了,比如 C# 的 async/await,Kotlin 的協程(Coroutine) 以及 Go 中的 Goroutine。隨著 Node.js 8 的發布,期待已久的異步函數功能終于來臨。
在本教程結束的時候,你應該可以回答下面的問題:
Node.js 中的 async/await 是自發明面包切片以來最美好的事情嗎?
Node 中的異步函數是什么鬼?
異步函數聲明返回 AsyncFunction 對象。這在某種意義上來說與 Generator 相似——它們的執行可以被中止。唯一的不同之處在于他們異步函數總是返回 Promise 而不是 { value: any, done: Boolean } 對象。實際上,異步函數與你從 co 包中體驗到的功能非常相似。
在異步函數中你可以等待(await) 任何 Promise 或捕獲其拒絕(reject) 的原因。
因此,如果你有像下面這們用 Promise 實現的邏輯:
〔譯者注:文本中的代碼都根據譯者推薦的格式進行了格式化,這不影響代碼的意義〕
function handler(req, res) {
return request("https://user-handler-service")
.catch((err) => {
logger.error("Http error", err);
error.logged = true;
throw err;
})
.then((response) => Mongo.findOne({ user: response.body.user }))
.catch((err) => {
!error.logged && logger.error("Mongo error", err);
error.logged = true;
throw err;
})
.then((document) => executeLogic(req, res, document))
.catch((err) => {
!error.logged && console.error(err);
res.status(500).send();
});
}
你可以使用 async/await 將其修改得更像在編寫同步代碼:
async function handler(req, res) {
let response;
try {
response = await request("https://user-handler-service");
} catch (err) {
logger.error("Http error", err);
return res.status(500).send();
}
let document;
try {
document = await Mongo.findOne({ user: response.body.user });
} catch (err) {
logger.error("Mongo error", err);
return res.status(500).send();
}
executeLogic(document, req, res);
}
在較舊的 V8 中,如果沒有處理拒絕的 Promise,它只是悄悄地被丟棄了。現在至少你會從 Node 中得到一個警告,因此你不必為此擔心而去創建一個監聽程序。然而,如果你不能處理錯誤而讓應用處于一個未知的狀態,那么推薦的做法是讓你的應用崩潰掉:
process.on("unhandledRejection", (err) => {
console.error(err);
process.exit(1);
});
使用異步函數的模式
確實有一些案例,如果能像寫同步程序一樣方便的進行異步操作就好了。使用 Promise 和回調來處理它們會需要復雜的模式或引用其它庫。
這里有一些例子,需要在循環中異步獲取數據,或使用 if-else 條件。
通過指數補償進行重試
使用 Promise 實現重試邏輯非常笨拙:
function requestWithRetry(url, retryCount) {
if (retryCount) {
return new Promise((resolve, reject) => {
const timeout = Math.pow(2, retryCount);
setTimeout(() => {
console.log("Waiting", timeout, "ms");
_requestWithRetry(url, retryCount)
.then(resolve)
.catch(reject);
}, timeout);
});
} else {
return _requestWithRetry(url, 0);
}
}
function _requestWithRetry(url, retryCount) {
return request(url, retryCount)
.catch((err) => {
if (err.statusCode && err.statusCode >= 500) {
console.log("Retrying", err.message, retryCount);
return requestWithRetry(url, ++retryCount);
}
throw err;
});
}
requestWithRetry("http://localhost:3000")
.then((res) => {
console.log(res);
})
.catch(err => {
console.error(err);
});
這個代碼看著就頭痛。我們可以使用 async/await 來重寫這段代碼,這樣的代碼會簡單得多。
function wait(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, timeout);
});
}
async function requestWithRetry(url) {
const MAX_RETRIES = 10;
for (let i = 0; i <= MAX_RETRIES; i++) {
try {
return await request(url);
} catch (err) {
const timeout = Math.pow(2, i);
console.log("Waiting", timeout, "ms");
await wait(timeout);
console.log("Retrying", err.message, i);
}
}
}
這樣的代碼更讓人賞心悅目,不是嗎?
中間值
這個例子不像之前的例子那么可怕,但是如果你有 3 個異步函數在順序上存在依賴關系,你恐怕不得不從幾個不太好看的解決辦法中選擇一個。
functionA 返回 Promise,而調用 functionB 需要那個值,然后 functionC 需要從 functionA 和 functionB 的 Promise 中帶回來的值。
辦法 1: .then 圣誕樹
function executeAsyncTask() {
return functionA()
.then((valueA) => {
return functionB(valueA)
.then((valueB) => {
return functionC(valueA, valueB);
});
});
}
在這個辦法中,我們通過 3 層代碼獲得 valueA ,同時從上一個 Promise 獲得 valueB 。我們不能讓這個圣誕樹扁平化,否則就不會形成閉包,在調用 functionC 的時候就拿不到 valueA 。
〔譯者注:翻譯過程中去掉了一些疑似廣告的鏈接〕
辦法 2:移到上層作用域
function executeAsyncTask() {
let valueA;
return functionA()
.then((v) => {
valueA = v;
return functionB(valueA);
})
.then((valueB) => {
return functionC(valueA, valueB);
});
}
在圣誕樹中,我們使用了更高導的作用域使 valueA 有效。這里的情況類似,只不過我們現在把 valueA 定義在所有 .then 之外,然后我們可以將第一個 Promise 的確定值賦給它。
這個辦法當然有效,即扁平化了 .then 鏈又保持了正確的語義。然而,它也帶來了新的缺陷,比如 valueA 會在函數其它地方使用。我們需要使用兩個變量 —— valueA 和 v —— 它們是同一個值。
辦法 3:不必要的數組
function executeAsyncTask() {
return functionA()
.then(valueA => {
return Promise.all([valueA, functionB(valueA)]);
})
.then(([valueA, valueB]) => {
return functionC(valueA, valueB);
});
}
把 valueA 與 functionB 產生的 Promise 一起放在數組中,當然是為了使樹扁平化。它們可能是完全不同的類型,所以它們根本不應該放在一個數組中的可能性非常大。
辦法 4:寫一個輔助函數
const converge = (...promises) => (...args) => {
let [head, ...tail] = promises;
if (tail.length) {
return head(...args)
.then((value) => converge(...tail)(...args.concat([value])));
} else {
return head(...args);
}
};
functionA(2)
.then((valueA) => converge(functionB, functionC)(valueA));
你當然可以耍點小聰明,寫一個輔助函數來隱藏上下文,但這樣的代碼會非常難讀,對于那些并不精通函數式編程技法的人來說,可能不太容易理解。
使用 async/await 就什么問題都沒有了:
async function executeAsyncTask() {
const valueA = await functionA();
const valueB = await functionB(valueA);
return function3(valueA, valueB);
}
使用 async/await 處理多個并行請求
這與前面的例子相似。這里你是想同時執行幾個異步任務,并不同的地方使用它們的結果值,使用 async/await 很容易解決:
async function executeParallelAsyncTasks() {
const [valueA, valueB, valueC]
= await Promise.all([
functionA(),
functionB(),
functionC()
]);
doSomethingWith(valueA);
doSomethingElseWith(valueB);
doAnotherThingWith(valueC);
}
正如我們在這個示例中看到的,我們不需要把這些值移到上層作用域,也不需要創建毫無語義的數組來傳遞這些值。
數組迭代方法
你可以結合異步函數使用 map 、 filter 和 reduce ,不過它們的行為并不直觀。猜猜下面的腳本會在控制臺打印出什么:
- map
function asyncThing(value) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(value), 100);
});
}
async function main() {
return [1, 2, 3, 4].map(async (value) => {
const v = await asyncThing(value);
return v * 2;
});
}
main()
.then(v => console.log(v))
.catch(err => console.error(err));
- filter
function asyncThing(value) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(value), 100);
});
}
async function main() {
return [1, 2, 3, 4].filter(async (value) => {
const v = await asyncThing(value);
return v % 2 === 0;
});
}
main()
.then(v => console.log(v))
.catch(err => console.error(err));
- reduce
function asyncThing(value) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(value), 100);
});
}
async function main() {
return [1, 2, 3, 4].reduce(async (acc, value) => {
return await acc + await asyncThing(value);
}, Promise.resolve(0));
}
main()
.then(v => console.log(v))
.catch(err => console.error(err));
答案:
[ Promise { }, Promise { }, Promise { }, Promise { } ]
[ 1, 2, 3, 4 ]
10
如果你記錄 map 迭代過程中的返回值,你會看到我們期望的數組: [ 2, 4, 6, 8 ] 。唯一的問題在于,每個都被 AsyncFunction 封裝成了 Promise。
因此,如果你想得到正確的值,就需要用 Promise.all 對返回的數組進行解封。
main()
.then(v => Promise.all(v))
.then(v => console.log(v))
.catch(err => console.error(err));
本來,你應該先等待 Promise 確定值,然后映射值:
function main() {
return Promise.all([1, 2, 3, 4]
.map((value) => asyncThing(value)));
}
main()
.then(values => values.map((value) => value * 2))
.then(v => console.log(v))
.catch(err => console.error(err));
這樣看起來簡單一些,不是嗎?
如果在一個長時間運行的異步任務中,需要迭代一些一些長時間運行的同步邏輯,那么 async/await 仍然會非常有用。
這樣一來,只要有一個值你就可以開始計算 —— 不必等到所有 Promise 都解決了才開始計算。即使結果仍然被封裝在 Promise 中,它們仍然比順序執行快得多。
filter 呢?很明顯沒對...
很好,你猜到了:雖然返回值是 [ false, true, false, true ] ,但它們被封裝成 Promise,所以會從原來的數組取回所有值〔譯者注:因為 Promise 對象會被判為 true 〕。很不幸,要修正這個錯誤,就需要要確定所有值,然后再過濾。
〔譯者注:譯者饒有興趣的來補充了一個實現,貌似不簡單〕
// 譯者補充的實現 (只修改了 main 函數)
async function main() {
const promises = [1, 2, 3, 4]
.map(async value => {
const v = await asyncThing(value);
return {
value: value,
predicate: v % 2 === 0
};
});
return (await Promise.all(promises))
.filter(m => m.predicate)
.map(m => m.value);
}
歸約(reduce)相當直接。但要記住,你需要用 Promise.resolve 封裝初始值,返回的積累值也會被封裝,需要等待( await )。
如果你希望使用函數式編程模式, async/await 可能不適合你。
.. 因為它的意圖明確地用于命令式編程模式。
為了讓 .then 鏈看起來更純粹,你可以使用 Ramda 的 pipeP 和 composeP 函數.
重寫基于回調的 Node.js 應用程序
異步函數默認返回 Promise ,所以你可以重寫基于回調的函數,讓它們使用 Promise,然后等待( await ) 解決。你可以使用 Node.js 的 util.promisify 函數將基于回調的函數轉換為基于 Promise 的函數。
重寫基于 Promise 的應用程序
簡單的 .then 鏈可以直接升級,所以你立刻就能使用 async/await 。
function asyncTask() {
return functionA()
.then((valueA) => functionB(valueA))
.then((valueB) => functionC(valueB))
.then((valueC) => functionD(valueC))
.catch((err) => logger.error(err));
}
可以改為
async function asyncTask() {
try {
const valueA = await functionA();
const valueB = await functionB(valueA);
const valueC = await functionC(valueB);
return await functionD(valueC);
} catch (err) {
logger.error(err);
}
}
使用 async/await 重寫 Node.js 應用
-
如果你喜歡經典的 if-else 條件和 for/while 循環,
-
如果你認同 try-catch 塊處理錯誤的方式,
你會很愉快的使用 async/await 來改寫服務。
正如我們看到的那樣,它可以讓某些模式更容易編寫也更容易閱讀,所以它肯定在某些情況下比 Promise.then() 鏈更合適。然而,如果你陷入了過去幾年的函數式編程熱,你可能想忽略這一語言特性。
那么你們都在想什么呢? async/await 是發明面包切片之后最好的事情,還是像 es2015 的 class 一樣有爭議呢?
你是否已經在生產中使用 async/await ,或者準備堅決不會碰它? 讓我們在下面的評論中討論吧。
來自:http://www.zcfy.cc/article/mastering-async-await-in-node-js-risingstack-4102.html