原文:blog.bitsrc.io/understandi…
做者:Arfat Salman
翻譯:前端小白javascript
首先咱們來討論下回調函數,回調函數沒什麼特別的,只是在未來的某個時候執行的函數。因爲JavScript的異步特性,在許多不能當即得到結果的地方都須要回調前端
這是一個Node.js異步讀取文件的例子:java
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
複製代碼
當咱們想要執行多個異步操做時,就會出現問題。想象如下的場景(全部操做都是異步的):git
Arfat
。 咱們讀取 profile_img_url
並從 someServer.com
獲取圖像。transformations.log
中記錄這個任務,並帶上時間戳代碼大體以下:es6
注意回調函數的嵌套的末尾 })
的層級關係,這種方式被戲稱做 回調地獄 或 回調金字塔。缺點是 ——github
爲了解決上述問題,JavaScript 提出了 Promise。如今,咱們可使用鏈式結構而不是回調函數嵌套的結構。下面是一個例子 ——數據庫
如今整個流程是自上而下而不是從左至右結構,這是一個優勢。可是 promise 仍然有一些缺點 ——json
.then
中咱們仍是要處理回調try/catch
,咱們要使用 .catch()
處理錯誤promises
會很是頭疼,不直觀咱們來演示下關於最後一個缺點:api
假設咱們有一個for循環,它以隨機間隔(0到n秒)打印0到10。咱們須要使用 promise
按0到10順序打印出來。例如,若是0打印須要6秒,1打印須要2秒,那麼1應該等待0打印完以後再打印,以此類推。數組
不要使用 async/await
或者 .sort
,以後咱們會解決這個問題
ES2017(ES8) 中引入了async函數,使promise應用起來很是簡單
async
函數的使用是基於 promise
promise
異步代碼的替代方案async/await
能夠避免使用 promise
鏈式調用所以,理解 async/await
必需要先了解 promise
async/await
包含兩個關鍵字 async
和 await
。async
用來使得函數能夠異步執行。async
可讓咱們在函數中使用 await
,除此以外,在任何地方使用 await
都屬於語法錯誤。
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
複製代碼
注意,在函數聲明中 async
關鍵字在函數聲明的前面。在箭頭函數中,async
關鍵字則位於 =
和圓括號的中間。
async 函數還能做爲對象的方法,或是在類的聲明中。
// As an object's method
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// In a class
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
構造函數實現)
這是另一種實現方式,不使用 async
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
複製代碼
上面代碼中,咱們手動返回了一個 promise
對象,沒有使用 async
關鍵字
更準確地說,async函數的返回值會被 Promise.resolve
包裹。
若是返回值是原始值,Promise.resolve
會返回一個promise化的值,若是返回值是一個promise對象,則直接返回這個對象
// in case of primitive values
const p = Promise.resolve('hello')
p instanceof Promise;
// true
// p is returned as is it
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做用於一個表達式。當表達式是一個promise時,async函數會暫停執行,直到該promise狀態變爲 resolved
。當表達式爲非promise值時,會使用 Promise.resolve
將其轉換爲promise,而後狀態變爲 resolved
。
// utility function to cause delay
// and get random value
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;
}
// Execute fn
fn().then(console.log);
複製代碼
讓咱們來逐行看看 fn
函數
const a = await 9
,內部會被解析爲 const a = await Promise.resolve(9)
await
, 因此函數執行會暫停,直到變量 a
獲得一個值,在promise 會將其resolve爲9delayAndGetRandom(1000)
會使 fn
函數暫停,直到1秒鐘以後 delayAndGetRandom
被resolve,因此,fn
函數的執行有效地暫停了 1 秒鐘delayAndGetRandom
返回一個隨機數。不管在resolve函數中傳遞什麼,它都被分配給變量 b
。c
值爲 5 ,而後使用 await delayAndGetRandom(1000)
又延時了 1 秒鐘。在這行代碼中咱們並無使用 Promise.resolve
返回值。a + b * c
結果,並用 Promise.resolve
包裹並返回注意:若是這裏函數的暫停和恢復使你想起了 ES6 generators
,那是由於 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));
// Implementation One (Using for-loop)
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);
});
// Implementation Two (Using Recursion)
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);
});
});
};
複製代碼
若是使用async函數,更簡單
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}
複製代碼
咱們在語法部分所瞭解的,一個未捕獲的 Error()
會被包裝在一個 rejected promise 中,可是,咱們能夠在 async 函數中同步地使用 try-catch
處理錯誤。讓咱們從這一實用的函數開始 ——
async function canRejectOrReturn() {
// wait one second
await new Promise(res => setTimeout(res, 1000));
// Reject with ~50% probability
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')
}
return 'perfect number';
}
複製代碼
canRejectOrReturn()
是一個異步函數,要麼 resolve 'perfect number'
,要麼 reject Error('Sorry, number too big')
看看下面的代碼
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
複製代碼
由於咱們在等待 canRejectOrReturn
執行,它的 rejection 會被轉換爲一個錯誤拋出,catch
會執行,也就是說 foo
函數結果要麼 resolve 爲 undefined
(由於咱們在 try
中沒有返回值),要麼 resolve 'error caught'
。由於咱們在 foo
函數中使用了 try-catch
處理錯誤,因此說 foo
函數的結果永遠不會是 rejected。
另外一個例子
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
複製代碼
注意。此次在 foo
函數裏面咱們返回而不是等待 canRejectOrReturn
,foo
要麼 resolve 'perfect number'
,要麼 reject Error('Sorry, number too big')
,catch
語句不會被執行
由於咱們 return
了 canRejectOrReturn
返回的 promise 對象,所以 foo
最終的狀態由 canRejectOrReturn
的狀態決定,你能夠將 return canRejectOrReturn()
分紅兩行代碼,來更清楚的瞭解,注意第一行沒有 await
try {
const promise = canRejectOrReturn();
return promise;
}
複製代碼
讓咱們來看看 return
和 await
一塊兒使用的狀況
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
複製代碼
在這種狀況下 foo
resolve 'perfect number'
,或者 resolve 'error caught'
,沒有 rejection,就像上面那個只有 await
的例子,在這裏咱們 resolve 了 canRejectOrReturn
返回的值,而不是 undefined
比也能夠將 return await canRejectOrReturn()
拆分來看
try {
const value = await canRejectOrReturn();
return value;
}
// ...
複製代碼
因爲 Promise 和 async/await 之間錯綜複雜的操做。 可能會有一些隱藏的錯誤,咱們來看看 -
有時候咱們會忘記在 promise 前面使用 await,或者忘記 return
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}
複製代碼
注意,若是咱們不使用 await
或者 return
,foo
老是會 resolve undefined
,不會等待一秒,可是 canRejectOrReturn()
中的 promise 的確被執行了。若是有反作用,也會產生,若是拋出錯誤或者 reject,UnhandledPromiseRejectionWarning
就會產生
咱們常常在 .map
和 .filter
中使用 async 函數做爲回調函數,假設咱們有一個函數 fetchPublicReposCount(username)
,能夠獲取一個 github 用戶擁有的公開倉庫的數量。咱們想要得到三名不一樣用戶的公開倉庫數量,讓咱們來看代碼 —
const url = 'https://api.github.com/users';
// Utility fn to fetch repo counts
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
其實是一個 promises
數組,這個數組包含着每次調用函數獲取用戶倉庫數量返回的 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
,並將它們保存到 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
時,返回值是一個包含全部結果的數組,而失敗的時候則返回最早被reject失敗狀態的值,只要有一個 promise 對象被 rejected,Promise.all
的返回值爲第一個被 rejected 的 promise 對象對應的返回值。可是,同時運行全部 promise 可能行不通。若是你想批量完成 promise。能夠參考 p-map 關於數量可控的併發操做。
async 函數很是重要。隨着 Async Iterators 的引入,async 函數將會應用得愈來愈廣。對於現代 JavaScript 開發人員來講掌握並理解 async 函數相當重要。但願這篇文章能對你有所幫助