含義
async 函數(shù)是什么钦听?一句話洒试,它就是 Generator 函數(shù)的語法糖。
依次讀取兩個(gè)文件朴上,可以寫成async函數(shù)變得更像同步函數(shù)
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
- async函數(shù)就是將 Generator 函數(shù)的星號(hào)(*)替換成async儡司,將yield替換成await,并自帶執(zhí)行器余指。
- async和await捕犬,比起星號(hào)和yield,語義更清楚了酵镜。async表示函數(shù)里有異步操作碉碉,await表示緊跟在后面的表達(dá)式需要等待結(jié)果。
- co模塊約定淮韭,yield命令后面只能是 Thunk 函數(shù)或 Promise 對(duì)象垢粮,而async函數(shù)的await命令后面,可以是 Promise 對(duì)象和原始類型的值(數(shù)值靠粪、字符串和布爾值蜡吧,但這時(shí)會(huì)自動(dòng)轉(zhuǎn)成立即 resolved 的 Promise 對(duì)象
- async函數(shù)的返回值是 Promise 對(duì)象毫蚓,這比 Generator 函數(shù)的返回值是 Iterator 對(duì)象方便多了。你可以用then方法指定下一步的操作昔善。
async函數(shù)完全可以看作多個(gè)異步操作元潘,包裝成的一個(gè) Promise 對(duì)象,而await命令就是內(nèi)部then命令的語法糖君仆。
基本用法
async函數(shù)返回一個(gè) Promise 對(duì)象翩概,可以使用then方法添加回調(diào)函數(shù)。當(dāng)函數(shù)執(zhí)行的時(shí)候返咱,一旦遇到await就會(huì)先返回钥庇,等到異步操作完成,再接著執(zhí)行函數(shù)體內(nèi)后面的語句咖摹。
多種場(chǎng)景下的寫法:
// 函數(shù)聲明
async function foo() {}
// 函數(shù)表達(dá)式
const foo = async function () {};
// 對(duì)象的方法
let obj = { async foo() {} };
// 箭頭函數(shù)
const foo = async () => {};
語法
關(guān)于async函數(shù)的返回值問題
async函數(shù)返回一個(gè) Promise 對(duì)象
async函數(shù)內(nèi)部return語句返回的值评姨,會(huì)成為返回的Promise對(duì)象的then方法回調(diào)函數(shù)的參數(shù)
async函數(shù)內(nèi)部拋出錯(cuò)誤,會(huì)導(dǎo)致返回的 Promise 對(duì)象變?yōu)閞eject狀態(tài)萤晴。拋出的錯(cuò)誤對(duì)象會(huì)被catch方法回調(diào)函數(shù)接收到参咙。
async函數(shù)返回的 Promise 對(duì)象,必須等到內(nèi)部所有await命令后面的 Promise 對(duì)象執(zhí)行完硫眯,才會(huì)發(fā)生狀態(tài)改變,除非遇到return語句或者拋出錯(cuò)誤择同。也就是說两入,只有async函數(shù)內(nèi)部的異步操作執(zhí)行完,才會(huì)執(zhí)行返回的Promise對(duì)象的then方法指定的回調(diào)函數(shù)敲才。
關(guān)于await
await命令只能用在async函數(shù)之中裹纳,如果用在普通函數(shù),就會(huì)報(bào)錯(cuò)
await命令后面是一個(gè) Promise 對(duì)象紧武,返回該對(duì)象的結(jié)果剃氧。如果不是 Promise 對(duì)象,就直接返回對(duì)應(yīng)的值阻星。
await命令后面是一個(gè)thenable對(duì)象(即定義then方法的對(duì)象)朋鞍,那么await會(huì)將其等同于 Promise 對(duì)象。
可以通過await 完成簡(jiǎn)單的休眠的是實(shí)現(xiàn)
// 用法 每個(gè)1秒依次輸出1到5
async function fn() {
for(let i = 1; i <= 5; i++) {
console.log(i);
await new Promise(resolve => {
setTimeout(resolve, 1000)
});
}
}
- await命令后面的 Promise 對(duì)象如果變?yōu)閞eject狀態(tài)妥箕,則reject的參數(shù)會(huì)被async函數(shù)返回的Promise實(shí)例的catch方法的回調(diào)函數(shù)接收到滥酥。
async function f() {
await Promise.reject('出錯(cuò)了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
- 任何一個(gè)await語句后面的 Promise 對(duì)象變?yōu)閞eject狀態(tài),那么整個(gè)async函數(shù)都會(huì)中斷執(zhí)行畦幢。如果不想前一個(gè)await 的 Promise的中斷執(zhí)行影響后面的await的異步操作坎吻,可以這么寫:
async function f() {
try {
await Promise.reject('出錯(cuò)了');
} catch(e) {
// 使用catch去捕獲錯(cuò)誤,這樣就不會(huì)影響下一個(gè)awiat Promise的執(zhí)行
// 也不會(huì)觸發(fā)f()這個(gè)Promise實(shí)例的catch方法
// 捕獲到錯(cuò)誤了就不會(huì)中斷執(zhí)行 也不會(huì)改變實(shí)例的狀態(tài)
}
return await Promise.resolve('hello world');
}
f()
.then(console.log) // 打印出 hello world
.catch(console.log) // 不會(huì)執(zhí)行
// 也可以不使用try...catch塊來捕獲錯(cuò)誤
// Promise.reject('出錯(cuò)了') 的返回值本身就是一個(gè)Promise
// 可以直接在后面鏈?zhǔn)秸{(diào)用catch方法來捕獲錯(cuò)誤宇葱,這樣也不會(huì)影響后面代碼的執(zhí)行
// 如果有多個(gè)await命令瘦真,更推薦將所有的await統(tǒng)一放在try...catch結(jié)構(gòu)中刊头。
// 這樣一個(gè)catch就能處理多個(gè)await 可能出現(xiàn)的錯(cuò)誤
下面的例子使用try...catch結(jié)構(gòu),實(shí)現(xiàn)多次重復(fù)嘗試
const superagent = require('superagent');
const NUM_RETRIES = 3;
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error');
// 如果一次成功就直接跳出循環(huán)
// 如果發(fā)生錯(cuò)誤了 就會(huì)跳過break 直接執(zhí)行catch
// 然后再次進(jìn)入循環(huán) 再次發(fā)起請(qǐng)求
// NUM_RETRIES 定義了這個(gè)重復(fù)嘗試的次數(shù)
// 如果愿意 可以設(shè)置循環(huán)條件達(dá)到直到請(qǐng)求成功再跳出循環(huán)的目的
break;
} catch(err) {}
}
console.log(i); // 3
}
test();
- 多個(gè)await命令后面的異步操作诸尽,如果不存在繼發(fā)關(guān)系原杂,最好讓它們同時(shí)觸發(fā)。
let foo = await getFoo();
let bar = await getBar();
// 如果在async函數(shù)中這樣寫 這兩個(gè)異步操作 getBar會(huì)等到getFoo執(zhí)行完成后執(zhí)行
// 如果 getBar 與 getFoo 并不存在繼發(fā)方式弦讽,這無疑會(huì)影響性能
// 如果不存在繼發(fā)關(guān)系污尉,最好讓它們同時(shí)觸發(fā)
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo(); // 這樣寫也是一樣 因?yàn)楫惒讲僮鞅煌瑫r(shí)啟動(dòng)了
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
- async 函數(shù)可以保留運(yùn)行堆棧
const a = () => {
var num = 1
Promise.resolve().then(() => console.log(x)); // 報(bào)錯(cuò)
console.log(num+1); // 2
};
a()
// 先輸出2 再報(bào)錯(cuò) 錯(cuò)誤堆棧不包過a()
函數(shù)a內(nèi)部運(yùn)行了一個(gè)異步任務(wù)。當(dāng)異步任務(wù)運(yùn)行的時(shí)候往产,函數(shù)a()不會(huì)中斷被碗,而是繼續(xù)執(zhí)行。等到b()運(yùn)行結(jié)束仿村,可能a()早就運(yùn)行結(jié)束了锐朴,b()所在的上下文環(huán)境已經(jīng)消失了。如果b()或c()報(bào)錯(cuò)蔼囊,錯(cuò)誤堆棧將不包括a()焚志。
可以改為async函數(shù)
const a = async () => {
await Promise.resolve().then(() => console.log(x)); // 報(bào)錯(cuò);
console.log(num+1); // 不會(huì)執(zhí)行
};
// 直接報(bào)錯(cuò) 錯(cuò)誤堆棧包括a()
async 函數(shù)的實(shí)現(xiàn)原理
async 函數(shù)的實(shí)現(xiàn)原理,就是將 Generator 函數(shù)和自動(dòng)執(zhí)行器畏鼓,包裝在一個(gè)函數(shù)里酱酬。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
// spawn函數(shù)就是自動(dòng)執(zhí)行器
return spawn(function* () {
// ...
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(); });
});
}
與其他異步處理方法的比較
async 函數(shù)的實(shí)現(xiàn)最簡(jiǎn)潔,最符合語義云矫,幾乎沒有語義不相關(guān)的代碼膳沽。它將 Generator 寫法中的自動(dòng)執(zhí)行器,改在語言層面提供让禀,不暴露給用戶挑社,因此代碼量最少。如果使用 Generator 寫法巡揍,自動(dòng)執(zhí)行器需要用戶自己提供痛阻。
假定某個(gè) DOM 元素上面,部署了一系列的動(dòng)畫腮敌,前一個(gè)動(dòng)畫結(jié)束阱当,才能開始后一個(gè)。如果當(dāng)中有一個(gè)動(dòng)畫出錯(cuò)糜工,就不再往下執(zhí)行斗这,返回上一個(gè)成功執(zhí)行的動(dòng)畫的返回值。
async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略錯(cuò)誤啤斗,繼續(xù)執(zhí)行 */
// 上面的await 后面的表達(dá)式發(fā)生錯(cuò)誤 就會(huì)進(jìn)入catch塊
// 就不會(huì)繼續(xù)進(jìn)行循環(huán) 從而執(zhí)行下面的return 返回上一個(gè)成功執(zhí)行的返回值
}
return ret;
}
按順序完成異步操作
場(chǎng)景:依次遠(yuǎn)程讀取一組 URL表箭,然后按照讀取的順序輸出結(jié)果。
// Promise的寫法
function logInOrder(urls) {
// 遠(yuǎn)程讀取所有URL
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
// 按次序輸出
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}
// async 函數(shù)實(shí)現(xiàn)
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
// 上面代碼確實(shí)大大簡(jiǎn)化,問題是所有遠(yuǎn)程操作都是繼發(fā)免钻。
// 只有前一個(gè) URL 返回結(jié)果彼水,才會(huì)去讀取下一個(gè) URL
async function logInOrder(urls) {
// 并發(fā)讀取遠(yuǎn)程URL
//
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序輸出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
雖然map方法的參數(shù)是async函數(shù),但它是并發(fā)執(zhí)行的极舔,因?yàn)橹挥衋sync函數(shù)內(nèi)部是繼發(fā)執(zhí)行凤覆,外部不受影響。后面的for..of循環(huán)內(nèi)部使用了await拆魏,因此實(shí)現(xiàn)了按順序輸出
頂層 await
目前盯桦,有一個(gè)語法提案,允許在模塊的頂層獨(dú)立使用await命令渤刃。這個(gè)提案的目的拥峦,是借用await解決模塊異步加載的問題。
// awaiting.js
let output;
(async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
模塊awaiting.js的輸出值output卖子,取決于異步操作略号。我們把異步操作包裝在一個(gè) async 函數(shù)里面,然后調(diào)用這個(gè)函數(shù)洋闽,只有等里面的異步操作都執(zhí)行玄柠,變量output才會(huì)有值,否則就返回undefined诫舅。
下面是加載這個(gè)模塊的寫法:
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100)); // NaN
setTimeout(() => console.log(outputPlusValue(100)), 1000); // 可能會(huì)正常輸出
// outputPlusValue()的執(zhí)行結(jié)果羽利,完全取決于執(zhí)行的時(shí)間。如果awaiting.js里面的異步操作沒執(zhí)行完刊懈,加載進(jìn)來的output的值就是undefined
目前的解決方法这弧,就是讓原始模塊輸出一個(gè) Promise 對(duì)象,從這個(gè) Promise 對(duì)象判斷異步操作有沒有結(jié)束俏讹。
// awaiting.js
let output;
export default (async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
// usage.js
import promise, { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
promise.then(() => {
// promise的then函數(shù)執(zhí)行的時(shí)候 說明output已經(jīng)有值了
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100)), 1000);
});
這種寫法比較麻煩,等于要求模塊的使用者遵守一個(gè)額外的使用協(xié)議畜吊,按照特殊的方法使用這個(gè)模塊泽疆。一旦你忘了要用 Promise 加載,只使用正常的加載方法玲献,依賴這個(gè)模塊的代碼就可能出錯(cuò)殉疼。而且,如果上面的usage.js又有對(duì)外的輸出捌年,等于這個(gè)依賴鏈的所有模塊都要使用 Promise 加載瓢娜。
頂層的await命令,就是為了解決這個(gè)問題礼预。它保證只有異步操作完成眠砾,模塊才會(huì)輸出值。
// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);
// 兩個(gè)異步操作在輸出的時(shí)候托酸,都加上了await命令褒颈。只有等到異步操作完成柒巫,這個(gè)模塊才會(huì)輸出值。
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
上面代碼的寫法谷丸,與普通的模塊加載完全一樣堡掏。也就是說,模塊的使用者完全不用關(guān)心刨疼,依賴模塊的內(nèi)部有沒有異步操作泉唁,正常加載即可。
這時(shí)揩慕,模塊的加載會(huì)import的時(shí)候就會(huì)等待依賴模塊(上例是awaiting.js)的異步操作完成亭畜,才執(zhí)行后面的代碼,有點(diǎn)像暫停在那里漩绵。所以贱案,它總是會(huì)得到正確的output,不會(huì)因?yàn)榧虞d時(shí)機(jī)的不同止吐,而得到不一樣的值宝踪。
下面是頂層await的一些使用場(chǎng)景。
// import() 方法加載
const strings = await import(`/i18n/${navigator.language}`);
// 數(shù)據(jù)庫(kù)操作
const connection = await dbConnector();
// 依賴回滾
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
注意碍扔,如果加載多個(gè)包含頂層await命令的模塊瘩燥,加載命令是同步執(zhí)行的。
// x.js
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");
// y.js
console.log("Y");
// z.js
import "./x.js";
import "./y.js";
console.log("Z");
打印結(jié)果是X1不同、Y厉膀、X2、Z二拐。這說明服鹅,z.js并沒有等待x.js加載完成,再去加載y.js百新。加載命令是同步執(zhí)行的企软。
頂層的await命令有點(diǎn)像,交出代碼的執(zhí)行權(quán)給其他的模塊加載饭望,等異步操作完成后仗哨,再拿回執(zhí)行權(quán),繼續(xù)向下執(zhí)行铅辞。