ES7的async函數就是Generator函數的語法糖坎弯,使得異步操作的流程更加清晰。本篇參照了阮一峰的《ES6標準入門》的async篇幅里的大量內容,這是一本不可多得的好書疯搅,建議大家購買學習。你可以從Github上獲取本篇例子代碼埋泵。
仍舊以讀取本地文件內容為例幔欧,用Generator函數定義依次讀取兩個文件的異步操作:
var fs = require('fs');
var readFile = function (fileName, options) {
return new Promise(function (resolve, reject){
fs.readFile(fileName, options, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* () {
var f1 = yield readFile('./apples.txt', 'utf8');
console.log(f1);
var f2 = yield readFile('./oranges.txt', 'utf8');
console.log(f2);
};
var g = gen();
g.next().value.then(function(data) {
g.next(data).value.then(function(data) {
g.next(data);
});
});
寫成async函數,就是下面這樣:
var asyncReadFile = async function (){
var f1 = await readFile('./apples.txt', 'utf8');
var f2 = await readFile('./oranges.txt', 'utf8');
console.log(f1);
console.log(f2);
};
asyncReadFile();
先看async函數的定義秋泄,和Generator相比語法上來說琐馆,將*星號改成了關鍵字async规阀。將yield改成了await恒序。這主要是為了讓其語義上更可讀一點。熟悉C++的程序員谁撼,看到*星號容易想到函數指針歧胁,用關鍵字async不容易有歧義滋饲。另外熟悉Java的程序員,看到y(tǒng)ield容易想到暫停線程喊巍,用await表示等待異步執(zhí)行的結果屠缭,結合async自動執(zhí)行的特性,顯然await的語義更加貼切崭参。
再看運行呵曹,相比Generator一步步next,async的運行可方便多了何暮,上面一行代碼搞定奄喂。因為async 函數本質上就是將 Generator 函數和自動執(zhí)行器,包裝在一個spawn函數里:
async function fn(args){... }
// 等同于
function fn(args){
return spawn(function*() { ... });
}
所有的async函數都可以寫成上面的第二種形式海洼,其中的spawn函數就是自動執(zhí)行器跨新。實現如下,基本就是co模塊的翻版:
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var 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(undefined); });
});
}
自動執(zhí)行的內容在Generator里有詳細介紹坏逢。就算不理解也沒關系域帐,不影響使用async。
async函數的await命令后面是Promise對象(如果是原始類型的值是整,會自動將其轉成Promise對象并立即將狀態(tài)設成Resolved肖揣,效果等于同步操作)。進一步說贰盗,async函數完全可以看作多個異步操作许饿,包裝成的一個Promise對象,而await命令就是內部then命令的語法糖舵盈。因為await命令后面是Promise對象陋率,需要考慮rejected的情況,畢竟誰也不能斷言異步操作中不會出現異常秽晚,所以最好把await命令包進try…catch中:
async function myAsyncFun() {
try {
await somePromise();
} catch (err) {
console.log(err);
}
}
//另一種寫法
async function myAsyncFun() {
await somePromise().catch(function (err) {console.log(err);});
}
為了提高效率瓦糟,async函數的await命令間如果不存在依賴關系,即赴蝇,后一個異步操作不依賴于前一個異步操作的結果的話菩浙,可以讓這些異步操作并發(fā)執(zhí)行。怎么并發(fā)呢句伶?依次寫await命令不會并發(fā)執(zhí)行劲蜻,而是依次執(zhí)行。要并發(fā)可以依賴于await命令后是一個Promise對象的事實考余,用Promise.all來包裝一下先嬉,例如:
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
上面這樣getFoo和getBar會同時觸發(fā)執(zhí)行,可以縮短程序的執(zhí)行時間楚堤。
async函數的返回值是一個Promise對象疫蔓,這比Generator函數的返回值是Iterator對象方便多了含懊。你可以為其添加then方法:
var f = async function () {
return 'hello world';
}
f().then(v => console.log(v)) // "hello world"
需要注意的是,async函數返回的Promise對象衅胀,必須等到內部所有await命令的Promise對象執(zhí)行完岔乔,才會發(fā)生狀態(tài)改變。即async函數內部的異步操作執(zhí)行完滚躯,才會執(zhí)行返回值的then方法雏门。例如:
var asyncReadFile = async function (){
var f1 = await readFile('./apples.txt', 'utf8');
var f2 = await readFile('./oranges.txt', 'utf8');
var arr = new Array();
arr.push(f1);
arr.push(f2);
return arr;
};
asyncReadFile().then(function(value) {
value.forEach(function(v){
console.log(v);
});
});
上例中等兩個異步操作讀取完文件內容后,才觸發(fā)then回調函數掸掏。
只要一個await語句后面的Promise變?yōu)镽ejected狀態(tài)剿配,那么整個async函數都會中斷執(zhí)行:
var f = async function () {
await Promise.reject('出錯了');
return await Promise.resolve('成功了'); // 不會被執(zhí)行
};
f().then(v => console.log(v))
.catch(e => console.log(e)); //出錯了
如果不想一個異步操作出錯導致整個async函數中斷,可以將await包在try…catch里阅束,這樣后續(xù)的await就繼續(xù)被執(zhí)行:
var f = async function () {
try {
await Promise.reject('出錯了');
} catch(e) {
return await Promise.resolve('成功了');
}
};
f().then(v => console.log(v))
.catch(e => console.log(e)); //成功了
另一種方法是await后面的Promise對象再跟一個catch來處理可能出現的異常:
var f = async function () {
await Promise.reject('出錯了').catch(e => console.log(e));
return await Promise.resolve('成功了');
};
f().then(v => console.log(v))
.catch(e => console.log(e));
//出錯了
//成功了
最后呼胚,參照阮一峰《ES6標準入門》的一個例子(幾乎照扒原書,請大家支持該書)息裸。來看Async函數與Promise蝇更,Generator函數的區(qū)別。假定某個DOM元素上面呼盆,部署了一系列的動畫年扩,前一個動畫結束,才能開始后一個访圃。如果當中有一個動畫出錯厨幻,就不再往下執(zhí)行,返回上一個成功執(zhí)行的動畫的返回值腿时。
首先是Promise的寫法况脆。
function chainAnimationsPromise(elem, animations) {
var ret = null; // 變量ret用來保存上一個動畫的返回值
var p = Promise.resolve(); // 新建一個空的Promise
for(var anim of animations) { // 使用then方法,添加所有動畫
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
return p.catch(function(e) { // 返回一個部署了錯誤捕捉機制的Promise
/* 忽略錯誤批糟,繼續(xù)執(zhí)行 */
}).then(function() {
return ret;
});
}
雖然Promise的寫法比回調函數的寫法大大改進格了,但是一眼看上去,代碼完全都是Promise的API(then徽鼎、catch等等)盛末,操作本身的語義反而不容易看出來。接著是Generator函數的寫法:
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
var ret = null;
try {
for(var anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略錯誤否淤,繼續(xù)執(zhí)行 */
}
return ret;
});
}
上面代碼使用Generator函數遍歷了每個動畫悄但,語義比Promise寫法更清晰,用戶定義的操作全部都出現在spawn函數的內部石抡。這個寫法的問題在于檐嚣,必須有一個任務運行器,自動執(zhí)行Generator函數汁雷,上面代碼的spawn函數就是自動執(zhí)行器净嘀,它返回一個Promise對象,而且必須保證yield語句后面的表達式侠讯,必須返回一個Promise挖藏。最后是Async函數的寫法:
async function chainAnimationsAsync(elem, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略錯誤,繼續(xù)執(zhí)行 */
}
return ret;
}
可以看到Async函數的實現最簡潔厢漩,最符合語義膜眠,幾乎沒有語義不相關的代碼。它將Generator寫法中的自動執(zhí)行器溜嗜,改在語言層面提供宵膨,不暴露給用戶,因此代碼量最少炸宵。如果使用Generator寫法辟躏,自動執(zhí)行器需要用戶自己提供。