舒適提示: 這裏有個 視頻,你能夠結合着文章看。node
從 callbacks 到 promises,再到 async 函數編程
在 promises 正式成爲 JavaScript 標準的一部分以前,回調被大量用在異步編程中,下面是個例子:bootstrap
function handler(done) {
validateParams((error) => {
if (error) return done(error);
dbQuery((error, dbResults) => {
if (error) return done(error);
serviceCall(dbResults, (error, serviceResults) => {
console.log(result);
done(error, serviceResults);
});
});
});
}
複製代碼
相似以上深度嵌套的回調一般被稱爲「回調黑洞」,由於它讓代碼可讀性變差且不易維護。 幸運地是,如今 promises 成爲了 JavaScript 語言的一部分,如下實現了跟上面一樣的功能:promise
function handler() {
return validateParams()
.then(dbQuery)
.then(serviceCall)
.then(result => {
console.log(result);
return result;
});
}
複製代碼
最近,JavaScript 支持了 async 函數,上面的異步代碼能夠寫成像下面這樣的同步的代碼:性能優化
async function handler() {
await validateParams();
const dbResults = await dbQuery();
const results = await serviceCall(dbResults);
console.log(results);
return results;
}
複製代碼
藉助 async 函數,代碼變得更簡潔,代碼的邏輯和數據流都變得更可控,固然其實底層實現仍是異步。(注意,JavaScript 仍是單線程執行,async 函數並不會開新的線程。)bash
NodeJS 裏 ReadableStreams 做爲另外一種形式的異步也特別常見,下面是個例子:框架
const http = require('http');
http.createServer((req, res) => {
let body = '';
req.setEncoding('utf8');
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
res.write(body);
res.end();
});
}).listen(1337);
複製代碼
這段代碼有一點難理解:只能經過回調去拿 chunks 裏的數據流,並且數據流的結束也必須在回調裏處理。若是你沒能理解到函數是當即結束但實際處理必須在回調裏進行,可能就會引入 bug。異步
一樣很幸運,ES2018 特性裏引入的一個很酷的 async 迭代器 能夠簡化上面的代碼async
const http = require('http');
http.createServer(async (req, res) => {
try {
let body = '';
req.setEncoding('utf8');
for await (const chunk of req) {
body += chunk;
}
res.write(body);
res.end();
} catch {
res.statusCode = 500;
res.end();
}
}).listen(1337);
複製代碼
你能夠把全部數據處理邏輯都放到一個 async 函數裏使用 for await…of 去迭代 chunks,而不是分別在 'data' 和 'end' 回調裏處理,並且咱們還加了 try-catch 塊來避免 unhandledRejection 問題。異步編程
以上這些特性你今天就能夠在生成環境使用!async 函數從 Node.js 8 (V8 v6.2 / Chrome 62) 開始就已全面支持,async 迭代器從 Node.js 10 (V8 v6.8 / Chrome 68) 開始支持。
從 V8 v5.5 (Chrome 55 & Node.js 7) 到 V8 v6.8 (Chrome 68 & Node.js 10),咱們致力於異步代碼的性能優化,目前的效果還不錯,你能夠放心地使用這些新特性。
性能提高取決於如下三個因素:
當咱們在 Node.js 8 裏 啓用 TurboFan 的後,性能獲得了巨大的提高。
同時咱們引入了一個新的垃圾回收器,叫做 Orinoco,它把垃圾回收從主線程中移走,所以對請求響應速度提高有很大幫助。
最後,Node.js 8 中引入了一個 bug 在某些時候會讓 await 跳過一些微 tick,這反而讓性能變好了。這個 bug 是由於無心中違反了規範致使的,可是卻給了咱們優化的一些思路。這裏咱們稍微解釋下:
const p = Promise.resolve();
(async () => {
await p; console.log('after:await');
})();
p.then(() => console.log('tick:a'))
.then(() => console.log('tick:b'));
複製代碼
上面代碼一開始建立了一個已經完成狀態的 promise p,而後 await 出其結果,又同時鏈了兩個 then,那最終的 console.log 打印的結果會是什麼呢?
由於 p 是已完成的,你可能認爲其會先打印 'after:await',而後是剩下兩個 tick, 事實上 Node.js 8 裏的結果是:
從某層面上來講,JavaScript 裏存在任務和微任務。任務處理 I/O 和計時器等事件,一次只處理一個。微任務是爲了 async/await 和 promise 的延遲執行設計的,每次任務最後執行。在返回事件循環(event loop)前,微任務的隊列會被清空。
根據 MDN,async 函數是一個經過異步執行並隱式返回 promise 做爲結果的函數。從開發者角度看,async 函數讓異步代碼看起來像同步代碼。
一個最簡單的 async 函數:
async function computeAnswer() {
return 42;
}
複製代碼
函數執行後會返回一個 promise,你能夠像使用其它 promise 同樣用其返回的值。
const p = computeAnswer();
// → Promise
p.then(console.log);
// prints 42 on the next turn
複製代碼
你只能在下一個微任務執行後才能獲得 promise p 返回的值,換句話說,上面的代碼語義上等價於使用 Promise.resolve 獲得的結果:
function computeAnswer() {
return Promise.resolve(42);
}
複製代碼
async 函數真正強大的地方來源於 await 表達式,它可讓一個函數執行暫停直到一個 promise 已接受(resolved),而後等到已完成(fulfilled)後恢復執行。已完成的 promise 會做爲 await 的值。這裏的例子會解釋這個行爲:
async function fetchStatus(url) {
const response = await fetch(url);
return response.status;
}
複製代碼
fetchStatus 在遇到 await 時會暫停,當 fetch 這個 promise 已完成後會恢復執行,這跟直接鏈式處理 fetch 返回的 promise 某種程度上等價。
function fetchStatus(url) {
return fetch(url).then(response => response.status);
}
複製代碼
鏈式處理函數裏包含了以前跟在 await 後面的代碼。
正常來講你應該在 await 後面放一個 Promise,不過其實後面能夠跟任意 JavaScript 的值,若是跟的不是 promise,會被制轉爲 promise,因此 await 42 效果以下:
async function foo() {
const v = await 42;
return v;
}
const p = foo();
// → Promise
p.then(console.log);
// prints `42` eventually
複製代碼
更有趣的是,await 後能夠跟任何 「thenable」,例如任何含有 then 方法的對象,就算不是 promise 均可以。所以你能夠實現一個有意思的 類來記錄執行時間的消耗:
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(() => resolve(Date.now() - startTime),
this.timeout);
}
}
(async () => {
const actualTime = await new Sleep(1000);
console.log(actualTime);
})();
複製代碼
一塊兒來看看 V8 規範 裏是如何處理 await 的。下面是很簡單的 async 函數 foo
async function foo(v) {
const w = await v;
return w;
}
複製代碼
首先,V8 會把這個函數標記爲可恢復的,意味着執行能夠被暫停並恢復(從 await 角度看是這樣的)。而後,會建立一個所謂的 implicit_promise(用於把 async 函數裏產生的值轉爲 promise)。
簡單說,await v 初始化步驟有如下組成:
1.把 v 轉成一個 promise(跟在 await 後面的)。
2.綁定處理函數用於後期恢復。
3.暫停 async 函數並返回 implicit_promise 給掉用者。
咱們一步步來看,假設 await 後是一個 promise,且最終已完成狀態的值是 42。而後,引擎會建立一個新的 promise 而且把 await 後的值做爲 resolve 的值。藉助標準裏的 PromiseResolveThenableJob 這些 promise 會被放到下個週期執行。
總結下,對於每個 await 引擎都會建立兩個額外的 promise(即便右值已是一個 promise),而且須要至少三個微任務。誰會想到一個簡單的 await 居然會有如此多冗餘的運算?!
所以,這裏可使用 promiseResolve 操做來處理,只有必要的時候纔會進行 promise 的封裝:
下面是簡化後的 await 執行過程
除了性能,JavaScript 開發者也很關心問題定位和修復,這在異步代碼裏一直不是件容易的事。Chrome DevTools 如今支持了異步棧追蹤:
最近咱們搞的 零成本異步棧追蹤 使得 Error#stack 包含了 async 函數的調用信息。「零成本」聽起來很讓人興奮,對吧?當 Chrome DevTools 功能帶來重大開銷時,它如何才能實現零成本?舉個例子,foo 裏調用 bar,bar 在 await 一個 promise 後拋一個異常:
async function foo() {
await bar();
return 42;
}
async function bar() {
await Promise.resolve();
throw new Error('BEEP BEEP');
}
foo().catch(error => console.log(error.stack));
複製代碼
這段代碼在 Node.js 8 或 Node.js 10 運行結果以下:
$ node index.js
Error: BEEP BEEP
at bar (index.js:8:9)
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
at startup (internal/bootstrap/node.js:266:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
複製代碼
注意到,儘管是 foo() 裏的調用拋的錯,foo 自己卻不在棧追蹤信息裏。若是應用是部署在雲容器裏,這會讓開發者很難去定位問題。
有意思的是,引擎是知道 bar 結束後應該繼續執行什麼的:即 foo 函數裏 await 後。剛好,這裏也正是 foo 暫停的地方。引擎能夠利用這些信息重建異步的棧追蹤信息。有了以上優化,輸出就會變成這樣:
$ node --async-stack-traces index.js
Error: BEEP BEEP
at bar (index.js:8:9)
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
at startup (internal/bootstrap/node.js:266:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
at async foo (index.js:2:3)
複製代碼
在棧追蹤信息裏,最上層的函數出如今第一個,以後是一些異步調用棧,再後面是 foo 裏面 bar 上下文的棧信息。這個特性的啓用能夠經過 V8 的 --async-stack-traces 參數啓用。
然而,若是你跟上面 Chrome DevTools 裏的棧信息對比,你會發現棧追蹤裏異步部分缺失了 foo 的調用點信息。這裏利用了 await 恢復和暫停位置是同樣的特性,但 Promise#then() 或 Promise#catch() 就不是這樣的。能夠看 Mathias Bynens 的文章 await beats Promise#then() 瞭解更多。
async 函數變快少不了如下兩個優化:
除此以外,咱們經過 零成本異步棧追蹤 提高了 await 和 Promise.all() 開發調試體驗。
咱們還有些對 JavaScript 開發者友好的性能建議:
多使用 async 和 await 而不是手寫 promise 代碼,多使用 JavaScript 引擎提供的 promise 而不是本身去實現。