深入理解ES7的async/await

在最開始學習ES6的Promise時,曾寫過一篇博文《promise和co搭配生成器函數(shù)方式解決js代碼異步流程的比較》,文章中對比了使用Promise和co模塊搭配生成器函數(shù)解決js異步的異同业筏。

在文章末尾纤怒,提到了ES7的async和await闽坡,只是當時只是簡單的提了一下,并未做深入探討蛛砰。

在前兩個月發(fā)布的Nodejs V7中,已添加了對async和await的支持黍衙,今天就來對這個東東做一下深入的探究泥畅。以更加優(yōu)雅的方法寫異步代碼。

async/await是什么

async/await可以說是co模塊和生成器函數(shù)的語法糖们豌。用更加清晰的語義解決js異步代碼涯捻。

熟悉co模塊的同學應該都知道,co模塊是TJ大神寫的一個使用生成器函數(shù)來解決異步流程的模塊望迎,可以看做是生成器函數(shù)的執(zhí)行器障癌。而async/await則是對co模塊的升級,內(nèi)置生成器函數(shù)的執(zhí)行器辩尊,不再依賴co模塊涛浙。同時,async返回的是Promise摄欲。

從上面來看轿亮,不管是co模塊還是async/await,都是將Promise作為最基礎的單元胸墙,對Promise不很了解的同學可以先深入了解一下Promise我注。

對比Promise,co,async/await

下面我們使用一個簡單的例子,來對比一下三種方式的異同迟隅,以及取舍但骨。

我們采用mongodb的nodejs驅動,查詢mongodb數(shù)據(jù)庫作為例子智袭,原因是mongodb的js驅動已經(jīng)默認實現(xiàn)了返回Promise奔缠,而不用我們單獨去包裝Promise了。

使用Promise鏈

MongoClient.connect(url + db_name).then(db => {
    return db.collection('blogs');
}).then(coll => {
    return coll.find().toArray();
}).then(blogs => {
    console.log(blogs.length);
}).catch(err => {
    console.log(err);
})

Promise的then()方法可以返回另一個Promise吼野,也可以返回一個同步的值校哎,如果返回的是一個同步值,將會被包裝成一個Promise。
上面的例子中闷哆,db.collection()將返回一個同步的值腰奋,即集合對象,但是被包裝成Promise阳准,將會透傳到下一個then()方法氛堕。
上面一個例子,是使用的Promise鏈野蝇。
先連接數(shù)據(jù)庫MongoClient.connect()返回一個Promise讼稚,然后在then()方法里獲得數(shù)據(jù)庫對象db,然后再獲取到coll對象再返回绕沈。在下一個then()方法獲得coll對象锐想,然后進行查詢,查詢結果返回乍狐,逐層調用then()方法赠摇,形成一個Promise鏈。
在這個Promise鏈上浅蚪,如果任何一個環(huán)節(jié)出現(xiàn)異常藕帜,都會被最后的catch()捕捉到。
可以說惜傲,這個使用Promise鏈寫的代碼洽故,比層層調用回調函數(shù)更優(yōu)雅,流程也更明確盗誊。先獲得數(shù)據(jù)庫對象时甚,再獲得集合對象,最后查詢數(shù)據(jù)哈踱。
但是這里有個不怎么“優(yōu)雅”的問題荒适,在于,每一個then()方法獲取的對象开镣,都是上一個then()方法返回的數(shù)據(jù)刀诬。而不能跨層訪問。
什么意思邪财,就是說在第三個then(blogs => {})中我們只能獲取到查詢的結果blogs舅列,而不能使用上面的db對象和coll對象。這個時候卧蜓,如果要打印出blogs列表后,要關閉數(shù)據(jù)庫db.close()怎么辦把敞?
這個時候弥奸,可以兩種解決方法:
第一種是,使用then()嵌套奋早。我們將Promise鏈打斷盛霎,使之嵌套赠橙,猶如使用回調函數(shù)的嵌套一般:

MongoClient.connect(url + db_name).then(db => {
    let coll = db.collection('blogs');
    coll.find().toArray().then(blogs => {
        console.log(blogs.length);
        db.close();
    }).catch(err => {
        console.log(err);
    });
}).catch(err => {
    console.log(err);
})

這里我們將兩個Promise嵌套,這樣在最后一個查詢操作里面愤炸,就可以調用外面的db對象了期揪。但是這中方式,并不推薦规个。原因很簡單凤薛,我們從一種回調函數(shù)地獄走向了另一種Promise回調地獄。
而且诞仓,我們要對每個Promise的異常進行捕捉缤苫,因為Promise沒有形成鏈。

還有一種方式墅拭, 是在每個then()方法里都將db傳過來:

MongoClient.connect(url + db_name).then(db => {
    return {db:db,coll:db.collection('blogs')};
}).then(result => {
    return {db:result.db,blogs:result.coll.find().toArray()};
}).then(result => {
    return result.blogs.then(blogs => {   //注意這里活玲,result.coll.find().toArray()返回的是一個Promise,因此這里需要再解析一層
        return {db:result.db,blogs:blogs}
    })
}).then(result => {
    console.log(result.blogs.length);
    result.db.close();
}).catch(err => {
    console.log(err);
});

我們在每個then()方法的返回中谍婉,都將db及其每次的其他結果組成一個對象返回舒憾。請注意,如果每次的結果都是一個同步的值還好說穗熬,但是如果是一個Promise值镀迂,每一個Promise都需要多做一層解析。
例如上面的一個例子死陆,第二個then()方法返回的{db:result.db,blogs:result.coll.find().toArray()}對象中招拙,blogs是一個Promise,在下一個then()方法中措译,我們無法直接引用博客列表數(shù)組值别凤,因此需要先調用then()方法解析一層,然后將兩個同步值db和blogs返回领虹。
注意规哪,這里涉及到了Promise的嵌套,不過一個Promise只嵌套一層then()塌衰。
這種方式诉稍,也是很蛋疼的一個方式,因為如果遇到then()方法中返回的不是同步的值最疆,而是Promise的話杯巨,我們需要多做很多工作。而且努酸,每次都透傳一個“多余”的db對象服爷,在邏輯上也有點冗余。

但除此之外,對于Promise鏈的使用仍源,如果遇到上面的問題心褐,好像也沒其他更好的方法解決了。我們只能根據(jù)場景去選擇一種“最優(yōu)”的方案笼踩,如果要使用Promise鏈的話逗爹。

鑒于Promise上面蛋疼的問題,TJ大神將ES6中的生成器函數(shù)嚎于,用co模塊包裝了一下掘而,以更優(yōu)雅的方式來解決上面的問題。

co搭配生成器函數(shù)

如果使用co模塊搭配生成器函數(shù)匾旭,那么上面的例子可以改寫如下:

const co = require('co');
co(function* (){
    let db = yield MongoClient.connect(url + db_name);
    let coll = db.collection('blogs');
    let blogs = yield coll.find().toArray();
    console.log(blogs.length);
    db.close();
}).catch(err => {
    console.log(err);
});

co是一個函數(shù)镣屹,將接受一個生成器函數(shù)作為參數(shù),去執(zhí)行這個生成器函數(shù)价涝。生成器函數(shù)中使用yield關鍵字來“同步”獲取每個異步操作的值女蜈。
上面代碼在代碼形式上,比上面使用Promise鏈要優(yōu)雅色瘩,我們消滅了回調函數(shù)伪窖,代碼看起來都是同步的。除了使用co和yield有點怪之外居兆。

使用co模塊覆山,我們要將所有的操作包裝成一個生成器函數(shù),然后使用co()去調用這個生成器函數(shù)泥栖〈乜恚看上去也還可以接受,但是ES的進化是不滿足于此的吧享,于是async/await被提到了ES7的提案魏割。

async/await

我們先看一下使用async/await改寫上面的代碼:

(async function(){
    let db = await MongoClient.connect(url + db_name);
    let coll = db.collection('blogs');
    let blogs = await coll.find().toArray();
    console.log(blogs.length);
    db.close();
})().catch(err => {
    console.log(err);
});

我們對比代碼可以看出,async/await和co兩種方式代碼極為相似钢颂。
co換成了async钞它,yield換成了await。同時生成器函數(shù)變成了普通函數(shù)殊鞭。
這種方式在語義上更加清晰明了遭垛,async表明這個函數(shù)是異步的,同時await表示要“等待”異步操作返回值操灿。
async函數(shù)返回一個Promise锯仪,上面的代碼其實是這樣:

let getBlogs = async function(){
    let db = await MongoClient.connect(url + db_name);
    let coll = db.collection('blogs');
    let blogs = await coll.find().toArray();
    db.close();
    return blogs;
};

getBlogs().then(result => {
    console.log(result.length);
}).catch(err => {
    console.log(err);
})

我們定義getBlogs為一個async函數(shù),最后返回得到的博客列表最終會被包裝成一個Promise返回趾盐,如上庶喜,我們直接調用getBlogs().then()方法可獲取async函數(shù)返回值幌蚊。

好了,上面我們簡單對比了一下三種解決異步方案溃卡,下面我們來深入了解一下async/await。

深入async/await

async返回值

async用于定義一個異步函數(shù)蜒简,該函數(shù)返回一個Promise瘸羡。
如果async函數(shù)返回的是一個同步的值,這個值將被包裝成一個理解resolve的Promise搓茬,等同于return Promise.resolve(value)犹赖。
await用于一個異步操作之前,表示要“等待”這個異步操作的返回值卷仑。await也可以用于一個同步的值峻村。

//返回一個Promise
let timer = async function timer(){
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            resolve('500');
        },500);
    });
}

timer().then(result => {
  console.log(result);  //500
}).catch(err => {
    console.log(err.message);
});
//返回一個同步的值
let sayHi = async function sayHi(){
  let hi = await 'hello world';   
  return hi;  //等同于return Promise.resolve(hi);
}

sayHi().then(result => {
  console.log(result);
});

上面這個例子返回是一個同步的值,字符串'hello world'锡凝,sayHi()是一個async函數(shù)粘昨,返回值被包裝成一個Promise,可以調用then()方法獲取返回值窜锯。
對于一個同步的值张肾,可以使用await,也可以不使用await锚扎。效果效果是一樣的吞瞪。具體用不用,看情況驾孔。
比如上面使用mongodb查詢博客那個例子芍秆,let coll = db.collection('blogs');,這里我們就沒有用await翠勉,因為這是一個同步的值妖啥。當然,也可以使用await眉菱,這樣會顯得代碼統(tǒng)一迹栓。雖然效果是一樣的。

async函數(shù)的異常

let sayHi = async function sayHi(){
    throw new Error('出錯了');
}
sayHi().then(result => {
  console.log(result);
}).catch(err => {
    console.log(err.message);   //出錯了
});

我們直接在async函數(shù)中拋出一個異常俭缓,由于返回的是一個Promise克伊,因此,這個異郴梗可以調用返回Promise的catch()方法捕捉到愿吹。

和Promise鏈的對比:
我們的async函數(shù)中可以包含多個異步操作,其異常和Promise鏈有相同之處惜姐,如果有一個Promise被reject()那么后面的將不會再進行犁跪。

let count = ()=>{
    return new Promise((resolve,reject) => {
        setTimeout(()=>{
            reject('故意拋出錯誤');
        },500);
    });
}

let list = ()=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve([1,2,3]);
        },500);
    });
}

let getList = async ()=>{
    let c = await count();
    let l = await list();
    return {count:c,list:l};
}
console.time('begin');
getList().then(result => {
    console.log(result);
}).catch(err => {
    console.timeEnd('begin');
    console.log(err);
});
//begin: 507.490ms
//故意拋出錯誤

如上面的代碼椿息,定義兩個異步操作,count和list坷衍,使用setTimeout延時500毫秒寝优,count故意直接拋出異常,從輸出結果來看枫耳,count()拋出異常后乏矾,直接由catch()捕捉到了,list()并沒有繼續(xù)執(zhí)行迁杨。

并行

使用async后钻心,我們上面的例子都是串行的。比如上個list()和count()的例子铅协,我們可以將這個例子用作分頁查詢數(shù)據(jù)的場景捷沸。
先查詢出數(shù)據(jù)庫中總共有多少條記錄,然后再根據(jù)分頁條件查詢分頁數(shù)據(jù)狐史,最后返回分頁數(shù)據(jù)以及分頁信息痒给。
我們上面的例子count()和list()有個“先后順序”,即我們先查的總數(shù)预皇,然后又查的列表侈玄。其實,這兩個操作并無先后關聯(lián)性吟温,我們可以異步的同時進行查詢序仙,然后等到所有結果都返回時再拼裝數(shù)據(jù)即可。

let count = ()=>{
    return new Promise((resolve,reject) => {
        setTimeout(()=>{
            resolve(100);
        },500);
    });
}

let list = ()=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve([1,2,3]);
        },500);
    });
}

let getList = async ()=>{
    let result = await Promise.all([count(),list()]);
    return result;
}
console.time('begin');
getList().then(result => {
    console.timeEnd('begin');  //begin: 505.557ms
    console.log(result);       //[ 100, [ 1, 2, 3 ] ]
}).catch(err => {
    console.timeEnd('begin');
    console.log(err);
});

我們將count()和list()使用Promise.all()“同時”執(zhí)行鲁豪,這里count()和list()可以看作是“并行”執(zhí)行的潘悼,所耗時間將是兩個異步操作中耗時最長的耗時。
最后得到的結果是兩個操作的結果組成的數(shù)組爬橡。我們只需要按照順序取出數(shù)組中的值即可治唤。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市糙申,隨后出現(xiàn)的幾起案子宾添,更是在濱河造成了極大的恐慌,老刑警劉巖柜裸,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缕陕,死亡現(xiàn)場離奇詭異,居然都是意外死亡疙挺,警方通過查閱死者的電腦和手機扛邑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铐然,“玉大人蔬崩,你說我怎么就攤上這事恶座。” “怎么了沥阳?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵跨琳,是天一觀的道長。 經(jīng)常有香客問我桐罕,道長湾宙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任冈绊,我火速辦了婚禮,結果婚禮上埠啃,老公的妹妹穿的比我還像新娘死宣。我一直安慰自己,他們只是感情好碴开,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布毅该。 她就那樣靜靜地躺著,像睡著了一般潦牛。 火紅的嫁衣襯著肌膚如雪眶掌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天巴碗,我揣著相機與錄音朴爬,去河邊找鬼。 笑死橡淆,一個胖子當著我的面吹牛召噩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逸爵,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼具滴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了师倔?” 一聲冷哼從身側響起构韵,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎趋艘,沒想到半個月后疲恢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡致稀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年冈闭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抖单。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡萎攒,死狀恐怖遇八,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情耍休,我是刑警寧澤刃永,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站羊精,受9級特大地震影響斯够,放射性物質發(fā)生泄漏。R本人自食惡果不足惜喧锦,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一读规、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧燃少,春花似錦束亏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至阳液,卻和暖如春怕敬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背帘皿。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工东跪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鹰溜。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓越庇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親奉狈。 傳聞我的和親對象是個殘疾皇子卤唉,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容