JavaScript異步Async

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í)行器需要用戶自己提供。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末土全,一起剝皮案震驚了整個濱河市捎琐,隨后出現的幾起案子,更是在濱河造成了極大的恐慌裹匙,老刑警劉巖瑞凑,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異概页,居然都是意外死亡籽御,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進店門惰匙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來技掏,“玉大人,你說我怎么就攤上這事项鬼×憬兀” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵秃臣,是天一觀的道長涧衙。 經常有香客問我,道長奥此,這世上最難降的妖魔是什么弧哎? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮稚虎,結果婚禮上撤嫩,老公的妹妹穿的比我還像新娘。我一直安慰自己蠢终,他們只是感情好序攘,可當我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布茴她。 她就那樣靜靜地躺著,像睡著了一般程奠。 火紅的嫁衣襯著肌膚如雪丈牢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天瞄沙,我揣著相機與錄音己沛,去河邊找鬼。 笑死距境,一個胖子當著我的面吹牛申尼,可吹牛的內容都是我干的。 我是一名探鬼主播垫桂,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼师幕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了诬滩?” 一聲冷哼從身側響起们衙,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎碱呼,沒想到半個月后蒙挑,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡愚臀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年忆蚀,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姑裂。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡馋袜,死狀恐怖,靈堂內的尸體忽然破棺而出舶斧,到底是詐尸還是另有隱情欣鳖,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布茴厉,位于F島的核電站泽台,受9級特大地震影響,放射性物質發(fā)生泄漏矾缓。R本人自食惡果不足惜怀酷,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嗜闻。 院中可真熱鬧蜕依,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至檐束,卻和暖如春辫秧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厢塘。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肌幽,地道東北人晚碾。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像喂急,于是被迫代替她去往敵國和親格嘁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,654評論 2 354

推薦閱讀更多精彩內容