終極解決Javascript非同步
異步操作是JavaScript編程的麻煩事,麻煩到一直有人提出各種各樣的方案,試圖解決這個問題。
從最早的回調函數,到Promise對象,再到Generator函數,每次都有所改進,但又讓人覺得不徹底。它們都有額外的複雜性,都需要理解抽象的底層運行機制。
異步I / O不就是讀取一個文件嗎,幹嘛要搞得這麼複雜?異步編程的最高境界,就是根本不用關心它是不是異步。
async函數就是隧道盡頭的亮光,很多人認為它是異步操作的終極解決方案。
異步函數是什麼?
一句話,async函數就是Generator函數的語法糖。
前文有一個Generator函數,依次讀取兩個文件。
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
寫成async函數,就是下面這樣。
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
一比較就會發現,異步函數就是將發生函數的星號(*)替換成異步,將產生替換成await,僅此而已。
異步函數的優點
async函數對發生器函數的改進,體現在以下三點。
(1)內置執行器。發生器函數的執行必須靠執行器,所以才有了co函數庫,而異步函數自帶執行器。也就是說,異步函數的執行,與普通函數一模一樣,只要一行。
var result = asyncReadFile();
(2)更好的語義。async和await,比起星號和yield,語義更清楚了.async表示函數里有異步操作,等待表示緊跟在後面的表達式需要等待結果。
(3)更廣的適用性。co 函數庫約定,yield命令後面只能是Thunk函數或Promise對象,而async函數的await命令後面,可以跟Promise對象和原始類型的值(數值,字符串和布爾值,但這時等同於同步操作)。
異步函數的實現
async函數的實現,就是將Generator函數和自動執行器,包裝在一個函數里。
async function fn(args){
// ...
}
// 等同於
function fn(args){
return spawn(function*() {
// ...
});
}
所有的異步函數都可以寫成上面的第二種形式,其中的spawn函數就是自動執行器。
下面給出spawn函數的實現,基本就是前文自動執行器的翻版。
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); });
});
}
async函數是非常新的語法功能,新到都不屬於ES6,而是屬於ES7。目前,它仍處於提案階段,但是轉碼器Babel和regenerator都已經支持,轉碼後就能使用。
async函數的用法
同生函數一樣,async函數返回一個Promise對象,可以使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到觸發的異步操作完成,再接著執行函數體內後面的語句。
下面是一個例子。
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result){
console.log(result);
});
上面代碼是一個獲取股票報價的函數,函數前面的異步關鍵字,表明該函數內部有異步操作。調用該函數時,會立即返回一個承諾對象。
下面的例子,指定多少毫秒後輸出一個值。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
上面代碼指定50毫秒以後,輸出“hello world”。
注意點
await命令後面的Promise對象,運行結果可能是rejected,所以最好把await命令放在try … catch代碼塊中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一種寫法
async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
});
}
await命令只能用在async函數之中,如果用在普通函數,就會報錯。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代碼會報錯,因為await用在普通函數之中。但是,如果將forEach方法的參數改為異步函數,也有問題。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 可能得到錯誤結果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代碼可能不會正常工作,原因是這時三個db.post操作將是並發執行,也就是同時執行,而不是繼續執行。正確的寫法是採用for循環。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
如果確實希望多個請求並發執行,可以使用Promise.all方法。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的寫法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
文章來源:async函數的含義和用法