- 原文地址:Deeply Understanding JavaScript Async and Await with Examples
- 原文做者:Arfat Salman
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:xionglong58
- 校對者:Baddyo,Mcskiller,fireairforce
首先來了解下回調函數。回調函數會在被調用後的某一時刻執行,除此以外與其餘普通函數並沒有差異。因爲 JavaScript 的異步特徵,在一些不能當即得到函數返回值的地方都須要使用回調函數。javascript
下面是一個 Node.js 讀取文件時的示例(異步操做)——前端
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
複製代碼
但當咱們要處理多重異步操做時問題就會凸顯出來。假設有下面的應用場景(其中的全部操做都是異步的)——java
Arfat
,讀取 profile_img_url
數據,而後把圖片從 someServer.com
上下載下來。Arfat
發送 email。transformations.log
並加上時間戳。上述過程的代碼大體以下 ——android
注意回調函數的嵌套和程序末尾 })
的層級。 鑑於結構上的類似性,這種方式被形象地稱做回調地獄或回調金字塔。這種方式的一些缺點是 ——ios
爲了解決上述問題,JavaScript 提出了 Promise。如今,咱們可使用鏈式結構取代回調函數嵌套的結構。下面是一個例子 ——git
回調流程由從左至右結構變成咱們所熟悉的自上而下的結構,這是一個優勢。可是 promise 仍然有一些缺點 ——es6
.then
中處理回調。try/catch
,咱們須要使用 .catch
處理錯誤。爲了證實上面的最後一個缺點,嘗試一下下面的挑戰吧!github
假設要在 for 循環中以任意時間間隔(0 到 n 秒)輸出數字 0 到 10。咱們將使用 promise 去順序打印 0 到 10,好比打印 0 須要 6 秒,打印 1 要延遲 2 秒,而 1 須要 0 打印完成以後才能打印,其它數字打印過程也相似。數據庫
固然,不要使用 async/await
或 .sort
方法,隨後咱們將會解決這一問題。json
async 函數在 ES2017 (ES8) 中引入,使得 promise 的應用更加簡單。
所以,在理解 async/await 概念以前你必需要對 promise 有所瞭解。
async/await 包含兩個關鍵字 async 和 await。async
用來使得函數能夠異步執行。async
使得在函數中可使用 await
關鍵字,除此以外,在任何地方使用 await
都屬於語法錯誤。
// 應用到普通的聲明函數
async function myFn() {
// await ...
}
// 應用到箭頭函數
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
複製代碼
注意,在函數聲明中 async
關鍵字位於聲明的前面。在箭頭函數中,async
關鍵字則位於 =
和圓括號的中間。
async 函數還能做爲對象的方法,或是像下面代碼同樣位於類中。
// 做爲對象方法
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// 位於類中
class Obj {
async getResource() {
return fetch('https://www.example.com');
}
}
複製代碼
注意:類的構造函數和 getters/setters 不能做爲 async 函數。
async 函數與普通 JavaScript 函數相比有如下區別 ——
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
複製代碼
函數 fn
的返回值 'hello'
,因爲咱們使用了 async
關鍵字,返回值 'hello' 被包裝成了一個 promise 對象(經過 Promise.resolve
實現)。
所以,不使用 async
關鍵字的具備同等做用的替代方案可寫做 ——
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
複製代碼
在上面的代碼中咱們手動返回了一個 promise 對象用於替換 async
關鍵字。
確切地說,async 函數的返回值將會被傳遞到 Promise.resolve
方法中。
若是返回值是一個原始值,Promise.resolve
則返回該值的一個 promise 版本。可是,若是返回值是 promise 對象,那麼 Promise.resolve
將原封不動地返回這個對象。
// 返回值是原始值的狀況
const p = Promise.resolve('hello')
p instanceof Promise;
// true
//p 被原封不動地返回
Promise.resolve(p) === p;
// true
複製代碼
在 async 函數中拋出一個錯誤會發生什麼?
好比 ——
async function foo() {
throw Error('bar');
}
foo().catch(console.log);
複製代碼
若是錯誤未被捕獲,foo()
函數會返回一個狀態爲 rejected 的 promise。不一樣於 Promise.resolve
,Promise.reject
會包裝錯誤並返回。詳情請看稍後的錯誤處理部分。
最終結果是,不論你想要返回什麼結果,最終在 async 函數外,你都會獲得一個 promise。
await
命令就像一個表達式同樣。當 await 後面跟着一個 promise 時,async 函數遇到 await 會停止運行,直到相應的 promise 狀態變成 resolved。當 await 後面跟的是原始值時,原始值會被傳入 Promise.resolve
而轉變成一個 promise 對象,而且狀態爲 resolved。
// 多功能函數:獲取隨機值/延時
const delayAndGetRandom = (ms) => {
return new Promise(resolve => setTimeout(
() => {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
return a + b * c;
}
// 執行函數 fn
fn().then(console.log);
複製代碼
讓咱們來逐行檢驗函數 fn
——
當函數 fn
被調用時,首先被執行的是 const a = await 9;
。它被隱式地轉換成 const a = await Promise.resolve(9);
。
因爲咱們使用了 await
命令,fn
函數會在此時會暫停到變量 a 得到值爲止。在該狀況下 Promise.resolve
方法返回值爲 9。
delayAndGetRandom(1000)
函數使得 fn
中的其它程序暫停執行,直到 1 秒鐘以後 delayAndGetRandom
狀態轉變成 resolved。因此,fn
函數的執行有效地暫停了 1 秒鐘。
此外,delayAndGetRandom
中的 resolve 函數返回一個隨機值。不管往 resolve
函數中傳入什麼值, 都會賦值給變量 b
。
一樣,變量 c
值爲 5
,而後使用 await delayAndGetRandom(1000)
又延時了 1 秒鐘。在這個例子中咱們並無使用 Promise.resolve
返回值。
最後咱們計算 a + b * c
的結果,經過 Promise.resolve
將該結果包裝成一個 promise,並將其做爲 async 函數的返回值。
注意: 若是上面程序的暫停和恢復操做讓你想起了 ES6 的 generator,那是由於 generator 也有不少優勢。
讓咱們使用 async/await 解決在前面提出的假設問題 ——
咱們定義了一個 async 函數 finishMyTask
,使用 await
去等待 queryDatabase
、sendEmail
、logTaskInFile
的操做結果。
若是咱們將 async/await 解決方案與使用 promise 的方案進行對比以後會發現代碼的數量很相近。可是 async/await 使得代碼在語法複雜性方面變得更簡單,不用去記憶多層回調函數以及 .then
/.catch
。
如今,就讓咱們解決上面所列的打印數字的挑戰。下面是兩種不一樣的解決方法 ——
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
// 方法一(使用 for 循環)
const printNumbers = () => new Promise((resolve) => {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) => {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// 方法二(使用回調)
const printNumbersRecursive = () => {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) => {
console.log(val);
return processNextPromise(i + 1);
});
});
};
複製代碼
你能夠在 repl.it console 上運行上面的代碼。
若是容許你使用 async 函數,那麼這個挑戰解決起來將會簡單得多。
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}
複製代碼
一樣,該方法也能夠在 repl.it console 上運行。
如同咱們在語法部分所見,一個未捕獲的 Error()
被包裝在一個 rejected promise 中。可是,咱們能夠在 async 函數中同步地使用 try-catch
處理錯誤。讓咱們從這一實用的函數開始 ——
async function canRejectOrReturn() {
// 等待一秒
await new Promise(res => setTimeout(res, 1000));
// 50% 的可能性是 Rejected 狀態
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')
}
return 'perfect number';
}
複製代碼
canRejectOrReturn()
是一個 async 函數,他可能返回 'perfect number'
也可能拋出錯誤('Sorry, number too big')。
咱們來看看示例代碼 ——
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
複製代碼
由於咱們在等待執行 canRejectOrReturn
函數的時候,canRejectOrReturn 函數體內的 promise 會轉移到 rejected 狀態而拋出錯誤,這將致使 catch
代碼塊被執行。也就是說 foo
函數運行結果爲 rejected
,返回值爲 undefined
(由於咱們在 try
中沒有返回值)或者 'error caught'
。由於咱們在 foo
函數中使用了 try-catch
處理錯誤,因此說 foo
函數的結果永遠不會是 rejected。
下面是另一個版本的例子 ——
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
複製代碼
注意這一次咱們使用了 return (而不是 await)將函數 canRejectOrReturn
從 foo
函數中返回。foo
函數運行結果是 resolved,返回值爲 'perfect number'
或者值爲 Error('Sorry, number too big')。catch
代碼塊永遠都不會被執行。
這是由於函數 foo
返回了 canRejectOrReturn
返回的 promise 對象。所以 foo
的 resolved 變成了 canRejectOrReturn
的 resolved。你能夠將 return canRejectOrReturn()
等價爲下面兩行程序去理解(注意第一行沒有 await)——
try {
const promise = canRejectOrReturn();
}
複製代碼
讓咱們看看 await
和 return
搭配使用時的狀況 ——
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
複製代碼
在上面的例子中,foo
函數運行結果爲 resolved,返回值爲 'perfect number'
或 'error caught'
。foo
函數的結果永遠不會是 rejected。 這就像上面那個只有 await
的例子。只是這裏將函數 canRejectOrReturn
的 rejected 結果返回了,而不是返回了 undefined
。
你能夠將語句 return await canRejectOrReturn();
拆開再看看效果 ——
try {
const value = await canRejectOrReturn();
return value;
}
// ...
複製代碼
因爲涉及 promise 和 async/await 之間錯綜複雜的操做,程序中可能會潛藏一些細微的差錯。讓咱們一塊兒看看吧 ——
有時候,在 promise 對象以前咱們忘記了使用 await
關鍵字,或者是忘記將 promise 對象返回。以下所示 ——
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}
複製代碼
注意咱們並無使用 await
或 return
。foo
函數運行結果爲返回值是 undefined
的 resolved,而且函數執行不會延遲 1 秒鐘。可是canRejectOrReturn() 中的 promise 的確被執行了。若是沒有反作用產生,這的確會發生。若是 canRejectOrReturn() 拋出錯誤或者狀態轉移爲 rejected,UnhandledPromiseRejectionWarning 錯誤將會產生。
咱們常常把 async 函數做爲.map
或 .filter
方法的回調。讓咱們舉個例子 — 假設咱們有一個函數 fetchPublicReposCount(username) 能夠獲取一個 github 用戶擁有的公開倉庫的數量。咱們想要得到三名不一樣用戶的公開倉庫數量,讓咱們來看代碼 —
const url = 'https://api.github.com/users';
// 使用 fn 函數獲取倉庫數量
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}
複製代碼
想要得到三名用戶 ['ArfatSalman', 'octocat', 'norvig'] 的公開倉庫數量。咱們可能會這樣作 ——
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
複製代碼
注意 async
在 .map
方法中。咱們可能但願變量 counts
存儲着的公開倉庫數量。可是,就如咱們以前所見,全部的 async 函數均返回 promise 對象。 所以,counts
其實是一個 promise 對象數組。.map
爲每個 username
調用異步函數,.map
方法將每次調用返回的 promise 結果保存在數組中。
咱們可能也會有其它解決方法,好比 ——
async function fetchAllCounts(users) {
const counts = [];
for (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}
複製代碼
咱們手動獲取了每個 count,並將它們 append 到 counts
數組中。程序的問題在於第一個用戶的 count 被獲取以後,第二個用戶的 count 才能被獲取。同一時間,只有一個公開倉庫數量能夠被獲取。
若是一個 fetch 操做耗時 300 ms,那麼 fetchAllCounts
函數耗時大概在 900 ms 左右。因而可知,程序耗時會隨着用戶數量的增長而線性增長。由於獲取不一樣用戶公開倉庫數量之間沒有依賴,咱們能夠將操做並行處理。
咱們能夠同時獲取用戶的公開倉庫數量,而不是順序獲取。咱們將使用 .map
方法和 Promise.all
。
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}
複製代碼
Promise.all
接受一個 promise 對象數組做爲輸入,返回一個 promise 對象。當全部 promise 對象的狀態都轉變成 resolved 時,返回值爲全部 promise 對應返回值組成的 promise 數組,只要有一個 promise 對象被 rejected,Promise.all
的返回值爲第一個被 rejected 的 promise 對象對應的返回值。可是,同時運行全部 promise 的操做可能行不通。可能你想批量執行 promise。你能夠考慮下使用 p-map 實現受限的併發。
async 函數變得很重要。隨着 Async Iterators 的引入,async 函數將會應用得愈來愈廣。對於現代 JavaScript 開發人員來講深刻理解 async 函數相當重要。我但願這篇文章能對你有所啓發。:)
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。