"承諾"的終極解決方案
有一種特殊的語(yǔ)法以一種更舒適的方式處理承諾,稱為“async/await”。它非常容易理解和使用。
Async functions
讓我們從async關(guān)鍵字開始。它可以放在函數(shù)前面,像這樣:
async function f() {
return 1;
}
函數(shù)前面的“async”一詞意味著一件簡(jiǎn)單的事情:函數(shù)總是返回promise。其他值自動(dòng)包裝在已解析的承諾中。
例如,該函數(shù)返回一個(gè)已解析的promise,其結(jié)果為1;讓我們測(cè)試它:
async function f() {
return 1;
}
f().then(alert); // 1
我們可以顯式地返回一個(gè)promise,它是一樣的:
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
因此,async確保該函數(shù)返回一個(gè)承諾,并將非承諾封裝在其中。很簡(jiǎn)單,對(duì)吧?但不僅如此。還有另一個(gè)關(guān)鍵字await,它只在異步函數(shù)內(nèi)部工作,非常酷。
Await
語(yǔ)法:
// works only inside async functions
let value = await promise;
關(guān)鍵字await使JavaScript等待,直到promise解決并返回結(jié)果。
下面是一個(gè)承諾在1秒內(nèi)解決的例子:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
函數(shù)的執(zhí)行“暫?!痹?*)行,當(dāng)promise結(jié)束時(shí)繼續(xù)執(zhí)行,result成為它的結(jié)果。所以上面的代碼顯示“done!”“一秒鐘之內(nèi)。
讓我們強(qiáng)調(diào)一下:await實(shí)際上是暫停函數(shù)的執(zhí)行,直到promise結(jié)束,然后使用promise結(jié)果繼續(xù)執(zhí)行。這不會(huì)消耗任何CPU資源,因?yàn)镴avaScript引擎可以同時(shí)執(zhí)行其他任務(wù):執(zhí)行其他腳本、處理事件等等。
這是一種比promise更優(yōu)雅的獲取promise結(jié)果的語(yǔ)法。然后,更容易讀和寫。
不能在常規(guī)函數(shù)中使用await
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
讓我們以承諾鏈接一章中的showAvatar()為例,并使用async/await重寫它:
我們需要替換。然后調(diào)用await。
我們也應(yīng)該使函數(shù)異步,以使它們工作。
async function showAvatar() {
// read our JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// read github user
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// show the avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
await在頂級(jí)代碼中不起作用
剛剛開始使用await的人往往會(huì)忘記這樣一個(gè)事實(shí):我們不能在頂級(jí)代碼中使用await。例如,這將不起作用:
// syntax error in top-level code
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
但是我們可以把它包裝成一個(gè)匿名異步函數(shù),像這樣:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
})();
等待接受“thenables”
像承諾。然后,await允許我們使用thenable對(duì)象(帶有可調(diào)用then方法的對(duì)象)。其理念是,第三方對(duì)象可能不是承諾,但與承諾兼容:如果它支持.那么,與await一起使用它就足夠了。
這是一個(gè)演示的Thenable類;下面的await接受它的實(shí)例:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// resolve with this.num*2 after 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// waits for 1 second, then result becomes 2
let result = await new Thenable(1);
alert(result);
}
f();
如果await獲得一個(gè)帶有.then的非Promise對(duì)象,它將調(diào)用該方法,并提供內(nèi)置函數(shù)resolve和reject作為參數(shù)(就像它對(duì)常規(guī)Promise executor所做的那樣)。然后await等待,直到其中一個(gè)被調(diào)用(在上面的例子中,它發(fā)生在行(*)中),然后繼續(xù)處理結(jié)果。
異步類方法
要聲明一個(gè)異步類方法,只需在它的前面加上async:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1
錯(cuò)誤處理
如果promise正常解析,則await promise返回結(jié)果。但是在拒絕的情況下,它拋出錯(cuò)誤,就像在那一行有一個(gè)throw語(yǔ)句一樣。
這段代碼:
async function f() {
await Promise.reject(new Error("Whoops!"));
}
和這個(gè)是一樣的:
async function f() {
throw new Error("Whoops!");
}
在實(shí)際情況下,承諾可能需要一段時(shí)間才會(huì)被拒絕。在這種情況下,在await拋出錯(cuò)誤之前會(huì)有延遲。
我們可以使用try..catch,和普通的throw一樣:
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
在出現(xiàn)錯(cuò)誤的情況下,控件跳轉(zhuǎn)到catch塊。我們也可以換行:
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
如果我們不 try…catch,然后異步函數(shù)f()調(diào)用生成的承諾將被拒絕。我們可以添加.catch來(lái)處理它:
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)
如果我們忘記在那里添加.catch,那么我們會(huì)得到一個(gè)未處理的promise錯(cuò)誤(在控制臺(tái)中可見)。我們可以使用全局unhandledrejection事件處理程序來(lái)捕獲這樣的錯(cuò)誤,如“帶承諾的錯(cuò)誤處理”一章所述。
async/await and promise.then/catch
當(dāng)我們使用async/await時(shí),我們很少需要.then,因?yàn)閍wait處理等待我們的操作。我們可以經(jīng)常try…catch代替.catch。這通常(但不總是)更方便。
但是在代碼的頂層,當(dāng)我們?cè)谌魏萎惒胶瘮?shù)之外時(shí),我們?cè)谡Z(yǔ)法上不能使用await,所以通常的做法是添加.then/catch來(lái)處理最終結(jié)果或失敗錯(cuò)誤,就像上面例子中的(*)行。
async/await works well with Promise.all
當(dāng)我們需要等待多個(gè)承諾時(shí),我們可以用承諾來(lái)包裝它們。一切都在等待:
// wait for the array of results
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
