ES7 中的 Node.js 異步等待
JavaScript(以及 Node.js)最令人興奮的特性之一是 async
/await
ES7 中引入的語法。雖然它基本上只是 Promises 之上的語法糖,但僅這兩個關鍵字就應該讓在 Node 中編寫異步代碼更容易接受。這幾乎消除了回調地獄的問題,甚至讓我們在異步代碼周圍使用控制流結構。
在本文中,我們將看看 Promises 有什麼問題,新的 await
功能可以提供幫助,以及如何開始使用它現在 .
Promise 的問題
JavaScript 中“承諾”的概念已經存在了一段時間,並且由於 Bluebird 和 q 等第三方庫,它已經使用了多年,更不用說最近在 ES6 中添加的原生支持了。
它們是回調地獄問題的一個很好的解決方案,但不幸的是它們並沒有解決所有的異步問題。雖然是一個很大的改進,但 Promise 讓我們想要更加簡化。
假設您想使用 Github 的 REST API 來查找項目的星數。在這種情況下,您可能會使用出色的 request-promise 庫。使用基於 Promise 的方法,您必鬚髮出請求並在傳遞給 .then()
的回調中返回結果 ,像這樣:
var request = require('request-promise');
var options = {
url: 'https://api.github.com/repos/scottwrobinson/camo',
headers: {
'User-Agent': 'YOUR-GITHUB-USERNAME'
}
};
request.get(options).then(function(body) {
var json = JSON.parse(body);
console.log('Camo has', json.stargazers_count, 'stars!');
});
這將打印出如下內容:
$ node index.js
Camo has 1,000,000 stars!
好吧,也許這個數字有點誇張,但你明白了;)
使用 Promises 只發出一個這樣的請求並不難,但是如果我們想對 GitHub 上的許多不同存儲庫發出相同的請求怎麼辦?如果我們需要圍繞請求添加控制流(如條件或循環)會發生什麼?隨著您的需求變得越來越複雜,Promise 變得越來越難以使用,並且最終仍然會使您的代碼變得複雜。它們仍然比普通回調更好,因為你沒有無限嵌套,但它們並不能解決你所有的問題。
對於更複雜的場景,如以下代碼中的場景,您需要善於將 Promises 鏈接在一起並了解何時何地 你的異步代碼被執行。
"use strict";
var request = require('request-promise');
var headers = {
'User-Agent': 'YOUR-GITHUB-USERNAME'
};
var repos = [
'scottwrobinson/camo',
'facebook/react',
'scottwrobinson/twentyjs',
'moment/moment',
'nodejs/node',
'lodash/lodash'
];
var issueTitles = [];
var reqs = Promise.resolve();
repos.forEach(function(r) {
var options = { url: 'https://api.github.com/repos/' + r, headers: headers };
reqs = reqs.then(function() {
return request.get(options);
}).then(function(body) {
var json = JSON.parse(body);
var p = Promise.resolve();
// Only make request if it has open issues
if (json.has_issues) {
var issuesOptions = { url: 'https://api.github.com/repos/' + r + '/issues', headers: headers };
p = request.get(issuesOptions).then(function(ibody) {
var issuesJson = JSON.parse(ibody);
if (issuesJson[0]) {
issueTitles.push(issuesJson[0].title);
}
});
}
return p;
});
});
reqs.then(function() {
console.log('Issue titles:');
issueTitles.forEach(function(t) {
console.log(t);
});
});
注意 :Github 積極地對未經身份驗證的請求進行速率限制,所以如果您在運行上述代碼幾次後被切斷,請不要感到驚訝。您可以通過傳遞客戶端 ID/秘密來增加此限制。
在撰寫本文時,執行此代碼將產生以下結果:
$ node index.js
Issue titles:
feature request: bulk create/save support
Made renderIntoDocument tests asynchronous.
moment issue template
test: robust handling of env for npm-test-install
只需添加一個 for
循環和一個 if
對我們的異步代碼的聲明使它更難閱讀和理解。這種複雜性只能維持很長時間,然後才會變得難以處理。
查看代碼,您能立即告訴我請求實際執行的位置,或者每個代碼塊的運行順序嗎?可能沒有仔細閱讀它。
使用 Async/Await 進行簡化
新的 async
/await
語法允許您仍然使用 Promises,但它消除了為鏈接的 then()
提供回調的需要 方法。將發送到 then()
的值 回調直接從異步函數返回,就好像它是一個同步阻塞函數一樣。
let value = await myPromisifiedFunction();
雖然看似簡單,但這是對異步 JavaScript 代碼設計的巨大簡化。實現這一點所需的唯一額外語法是 await
關鍵詞。因此,如果您了解 Promises 的工作原理,那麼理解如何使用這些新關鍵字就不會太難,因為它們建立在 Promises 的概念之上。你真正需要知道的是 任何 Promise 都可以是 await
-ed .值也可以是 await
-ed,就像 Promise 可以 .resolve()
在整數或字符串上。
讓我們比較一下基於 Promise 的方法和 await
關鍵詞:
承諾
免費電子書:Git Essentials
查看我們的 Git 學習實踐指南,其中包含最佳實踐、行業認可的標準以及隨附的備忘單。停止谷歌搜索 Git 命令並真正學習 它!
var request = require('request-promise');
request.get('https://api.github.com/repos/scottwrobinson/camo').then(function(body) {
console.log('Body:', body);
});
等待
var request = require('request-promise');
async function main() {
var body = await request.get('https://api.github.com/repos/scottwrobinson/camo');
console.log('Body:', body);
}
main();
如您所見,await
表示您要解析 Promise 並且 不返回實際的 Promise 對象 像往常一樣。執行此行時,request
call 將被放入事件循環的堆棧中,執行將讓給其他準備好處理的異步代碼。
async
定義包含異步代碼的函數時使用關鍵字。這是一個從函數返回 Promise 的指標,因此應該被視為異步的。
下面是一個簡單的用法示例(注意函數定義的變化):
async function getCamoJson() {
var options = {
url: 'https://api.github.com/repos/scottwrobinson/camo',
headers: {
'User-Agent': 'YOUR-GITHUB-USERNAME'
}
};
return await request.get(options);
}
var body = await getCamoJson();
現在我們知道如何使用 async
和 await
一起來看看之前更複雜的基於 Promise 的代碼現在是什麼樣子:
"use strict";
var request = require('request-promise');
var headers = {
'User-Agent': 'scottwrobinson'
};
var repos = [
'scottwrobinson/camo',
'facebook/react',
'scottwrobinson/twentyjs',
'moment/moment',
'nodejs/node',
'lodash/lodash'
];
var issueTitles = [];
async function main() {
for (let i = 0; i < repos.length; i++) {
let options = { url: 'https://api.github.com/repos/' + repos[i], headers: headers };
let body = await request.get(options);
let json = JSON.parse(body);
if (json.has_issues) {
let issuesOptions = { url: 'https://api.github.com/repos/' + repos[i] + '/issues', headers: headers };
let ibody = await request.get(issuesOptions);
let issuesJson = JSON.parse(ibody);
if (issuesJson[0]) {
issueTitles.push(issuesJson[0].title);
}
}
}
console.log('Issue titles:');
issueTitles.forEach(function(t) {
console.log(t);
});
}
main();
它現在肯定更具可讀性,因為它可以像許多其他線性執行語言一樣編寫。
現在唯一的問題是每個 request.get()
call 是串行執行的(意味著每個調用都必須等到前一個調用完成才能執行),因此我們必須等待更長的時間讓代碼完成執行才能獲得結果。更好的選擇是並行運行 HTTP GET 請求。這仍然可以通過利用 Promise.all()
來完成 就像我們以前會做的那樣。只需替換 for
使用 .map()
循環 調用並將生成的 Promises 數組發送到 Promise.all()
,像這樣:
// Init code omitted...
async function main() {
let reqs = repos.map(async function(r) {
let options = { url: 'https://api.github.com/repos/' + r, headers: headers };
let body = await request.get(options);
let json = JSON.parse(body);
if (json.has_issues) {
let issuesOptions = { url: 'https://api.github.com/repos/' + r + '/issues', headers: headers };
let ibody = await request.get(issuesOptions);
let issuesJson = JSON.parse(ibody);
if (issuesJson[0]) {
issueTitles.push(issuesJson[0].title);
}
}
});
await Promise.all(reqs);
}
main();
這樣您就可以利用並行執行的速度和 await
的簡單性 .
除了能夠使用循環和條件等傳統控制流之外,還有更多好處。這種線性方法讓我們回到使用 try...catch
處理錯誤的語句。使用 Promises 你必須使用 .catch()
方法,該方法有效,但可能會導致混淆,以確定它為哪些 Promise 捕獲了異常。
所以現在這個...
request.get('https://api.github.com/repos/scottwrobinson/camo').then(function(body) {
console.log(body);
}).catch(function(err) {
console.log('Got an error:', err.message);
});
// Got an error: 403 - "Request forbidden by administrative rules. Please make sure your request has a User-Agent header..."
...可以這樣表達:
try {
var body = await request.get('https://api.github.com/repos/scottwrobinson/camo');
console.log(body);
} catch(err) {
console.log('Got an error:', err.message)
}
// Got an error: 403 - "Request forbidden by administrative rules. Please make sure your request has a User-Agent header..."
雖然它的代碼量大致相同,但對於從另一種語言過渡到 JavaScript 的人來說,它更容易閱讀和理解。
立即使用異步
異步功能仍處於提案階段,但不用擔心,您仍然可以通過多種方式在代碼中使用它現在 .
V8
雖然它還沒有完全進入 Node,但 V8 團隊已經公開表示他們打算實現 async
/await
特徵。他們甚至已經提交了原型運行時實現,這意味著和諧支持應該不會落後太多。
通天塔
可以說,最流行的選擇是使用 Babel 及其各種插件來轉譯您的代碼。 Babel 非常受歡迎,因為它能夠使用他們的插件系統混合和匹配 ES6 和 ES7 功能。雖然設置起來有點複雜,但它也為開發人員提供了更多的控制權。
再生器
Facebook 的 regenerator 項目沒有 Babel 那麼多功能,但它是一種更簡單的異步轉譯工作方式。
我遇到的最大問題是它的錯誤不是很具有描述性。因此,如果您的代碼中存在語法錯誤,您將無法從 regenerator 中獲得太多幫助來查找它。除此之外,我一直很滿意。
跟踪器
我個人對此沒有任何經驗,但 Traceur(由 Google 提供)似乎是另一種流行的選擇,具有許多可用功能。您可以在此處找到更多信息,了解有關可以轉譯哪些 ES6 和 ES7 功能的詳細信息。
asyncawait
您可以使用的大多數選項都涉及轉譯或使用 V8 的夜間構建來獲得 async
在職的。另一種選擇是使用 asyncawait 包,它提供了一個類似於 await
的解析 Promises 的函數 特徵。這是一種很好的原生 ES5 方法,可以獲取外觀相似的語法。
結論
就是這樣!就個人而言,我對 ES7 中的這個特性感到最興奮,但 ES7 中還有一些其他很棒的特性你應該看看,比如類裝飾器和屬性。
你使用轉譯的 ES7 代碼嗎?如果是這樣,哪個功能對您的工作最有益?請在評論中告訴我們!