理解async/await

首先明確一個(gè)問題蚊伞,為什么 Node.js 需要異步編程?

JavaScript 是單線程的兜蠕,在發(fā)出一個(gè)調(diào)用時(shí)扰肌,在沒有得到結(jié)果之前,該調(diào)用就不返回熊杨,意思就是調(diào)用者主動等待調(diào)用結(jié)果曙旭,換句話說,就是必須等待上一個(gè)任務(wù)執(zhí)行完才能執(zhí)行下一個(gè)任務(wù)這種執(zhí)行模式叫:同步晶府。
Node.js 的主要應(yīng)用場景是處理高并發(fā)(單位時(shí)間內(nèi)極大的訪問量)和 I/O 密集場景(ps: I/O 操作往往非常耗時(shí)桂躏,所以異步的關(guān)鍵在于解決I/O耗時(shí)問題),如果采用同步編程川陆,問題就來了剂习,服務(wù)器處理一個(gè) I/O 請求需要大量的時(shí)間,后面的請求都將排隊(duì)较沪,造成瀏覽器端的卡頓鳞绕。異步編程能解決這個(gè)問題。
所謂異步尸曼,就是調(diào)用在發(fā)出后们何,這個(gè)調(diào)用就直接返回了,調(diào)用者不會立即得到結(jié)果并且可以繼續(xù)執(zhí)行后續(xù)操作控轿,而被調(diào)用者執(zhí)行得到結(jié)果后通過狀態(tài)冤竹、事件來通知調(diào)用者使用回調(diào)函數(shù)(callback)來處理這個(gè)調(diào)用。Node在處理耗時(shí)的 I/O 操作時(shí)茬射,將其交給其他線程處理鹦蠕,自己繼續(xù)處理其他訪問請求,當(dāng) I/O 操作處理好后就會通過事件通知 Node 用回調(diào)做后續(xù)處理在抛。
有個(gè)例子非常好:

你打電話問書店老板有沒有《分布式系統(tǒng)》這本書钟病,如果是同步通信機(jī)制,書店老板會說刚梭,你稍等肠阱,”我查一下",然后開始查啊查望浩,等查好了(可能是5秒辖所,也可能是一天)告訴你結(jié)果(返回結(jié)果)。而異步通信機(jī)制磨德,書店老板直接告訴你我查一下啊缘回,查好了打電話給你吆视,然后直接掛電話了(不返回結(jié)果)。然后查好了酥宴,他會主動打電話給你啦吧。在這里老板通過“回電”這種方式來回調(diào)。

下面幾種方式是異步解決方案的進(jìn)化過程:

CallBacks

回調(diào)函數(shù)就是函數(shù)A作為參數(shù)傳遞給函數(shù)B拙寡,并且在未來某一個(gè)時(shí)間被調(diào)用授滓。callback的異步模式最大的問題就是,理解困難加回調(diào)地獄(callback hell)肆糕,看下面的代碼的執(zhí)行順序:

A();
ajax('url1', function(){
    B();
    ajax('url2', function(){
        C();
    }
    D();
});
E();

其執(zhí)行順序?yàn)椋篈 => E => B => D => C般堆,這種執(zhí)行順序的確會讓人頭腦發(fā)昏,另外由于由于多個(gè)異步操作之間往往會耦合诚啃,只要中間一個(gè)操作需要修改淮摔,那么它的上層回調(diào)函數(shù)和下層回調(diào)函數(shù)都可能要修改,這就陷入了回調(diào)地獄始赎。而 Promise 對象就很好的解決了異步操作之間的耦合問題和橙,讓我們可以用同步編程的方式去寫異步操作。

Promise

Promise 對象是一個(gè)構(gòu)造函數(shù)造垛,用來生成promise實(shí)例魔招。Promise 代表一個(gè)異步操作,有三種狀態(tài):pending五辽,resolved(異步操作成功由pending變?yōu)閞esolved)办斑,rejected(異步操作失敗由pending變?yōu)閞ejected),一旦變?yōu)楹髢煞N狀態(tài)將不會再改變奔脐。Promise 對象作為構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù)俄周,而這個(gè)函數(shù)又接受 resolved 和 rejected 兩個(gè)函數(shù)做為參數(shù)吁讨,這兩個(gè)函數(shù)是JS內(nèi)置的髓迎,無需配置。resolved 函數(shù)在異步操作成功后調(diào)用建丧,將pending狀態(tài)變?yōu)閞esolved排龄,并將它的參數(shù)傳遞給回調(diào)函數(shù);rejected 函數(shù)在異步操作失敗時(shí)調(diào)用翎朱,將pending狀態(tài)變?yōu)閞ejected橄维,并將參數(shù)傳遞給回調(diào)函數(shù)。

Promise.prototype.then()

Promise構(gòu)造函數(shù)的原型上有一個(gè)then方法拴曲,它接受兩個(gè)函數(shù)作為參數(shù)争舞,分別是 resolved 狀態(tài)和 rejected 狀態(tài)的回調(diào)函數(shù),而這兩個(gè)回調(diào)函數(shù)接受的參數(shù)分別是Promise實(shí)例中resolved函數(shù)和rejected返回的結(jié)果, 另外第二個(gè)函數(shù)是可選的澈灼。

下面是一個(gè)示例:

const instance = new Promise((resolved, rejected) => {
    // 一些異步操作
    if(/*異步操作成功*/) {
      resolved(value);
    } else {
      rejected(error);
    }
  }
})
instance.then(value => {
  // do something...
}, error => {
  // do something...
})

注意Promise實(shí)例在生成后會立即執(zhí)行竞川,而then方法只有在所有同步任務(wù)執(zhí)行完后才會執(zhí)行(疑問)店溢,看看下面的例子:

const promise = new Promise((resolved, rejected) => {
  console.log('async task begins!');
  setTimeout(() => {
    resolved('done, pending -> resolved!');
  }, 1000);
})

promise.then(value => {
  console.log(value);
}) 

console.log('1.please wait');
console.log('2.please wait');
console.log('3.please wait');
// async task begins!
// 1.please wait
// 2.please wait
// 3.please wait
// done, pending -> resolved!

上面的實(shí)例可以看出,Promise新建后立即執(zhí)行委乌,所以首先輸出 'async task begins!'床牧,隨后定義了一個(gè)異步操作 setTimeout,1秒后執(zhí)行遭贸,所以無需等待戈咳,向下執(zhí)行,而then方法指定的回調(diào)函數(shù)要在所有同步任務(wù)執(zhí)行完后才執(zhí)行壕吹,所以先輸出了3個(gè)'please wait'著蛙,最后輸出'done, pending -> resolved!'。(此處省略了then方法中的rejected回調(diào)耳贬,一般不在then中做rejected狀態(tài)的處理册踩,而使用catch方法專門處理錯(cuò)誤)

鏈?zhǔn)秸{(diào)用then方法

then方法會將返回值傳遞給下一個(gè)回調(diào)(即return后面的值),如果返回的是Promise實(shí)例效拭,則可以繼續(xù)調(diào)用then方法暂吉,而如果返回的是值,則相當(dāng)于執(zhí)行了 promise.resolve()方法缎患,也可以繼續(xù)調(diào)用then方法慕的,這就是then的鏈?zhǔn)綄懛ǎ错樞驅(qū)崿F(xiàn)一系列的異步操作挤渔,這樣就可以用同步編程的形式去實(shí)現(xiàn)異步操作肮街,來看下面的例子:

function timeAdder(n) {
  return new Promise((resolved, rejected) => {
    setTimeout(() => {
      resolved(n + 200);
    }, n)
  })
}

timeAdder(0)
  .then(n => {
    console.log(`time1 is ${n}`);
    return timeAdder(n);    // 最終 resolved 函數(shù)中的參數(shù)將作為值傳遞給下一個(gè)then
  })
  // n 是上一個(gè)then傳遞出來的參數(shù)
  .then(n => {                
    console.log(`time2 is ${n}`);
    return timeAdder(n);
  })
  .then(n => {
    console.log(`time3 is ${n}`);
    return timeAdder(n);
  })
// time1 is 200
// time3 is 400
// time3 is 600

可以看到使用鏈?zhǔn)絫hen的寫法,將異步操作變成了同步的形式判导,但是也帶來了新的問題嫉父,就是異步操作變成了很長的then鏈,新的解決方法就是Generator眼刃,這里跨過它直接說它的語法糖:async/await绕辖。

async/await

async

async/await實(shí)際上是Generator的語法糖。顧名思義擂红,async關(guān)鍵字代表后面的函數(shù)中有異步操作仪际,await表示等待一個(gè)異步方法執(zhí)行完成。聲明一步函數(shù)只需在普通函數(shù)前面加一個(gè)關(guān)鍵字async即可昵骤,如:

async function funcA() {}

async 函數(shù)返回一個(gè)Promise對象(如果結(jié)果是值树碱,會經(jīng)過Promise包裝返回),因此asyn函數(shù)通過return返回的值变秦,會成為then方法的參數(shù):

async function funcA() {
  return 'hello!';
}

funcA().then(value => {
  console.log(value);
})
// hello!

單獨(dú)一個(gè)async函數(shù)成榜,其實(shí)與Promise實(shí)例執(zhí)行的功能是一樣的,來看看await都干了些啥蹦玫。

await

await 命令后面的值可以是Promise 對象或值赎婚,如果是值雨饺,就會被轉(zhuǎn)成一個(gè)立即resolve的Promise對象。async函數(shù)被調(diào)用后就立即執(zhí)行惑淳,但是一旦遇到await就會先返回额港,等到異步操作執(zhí)行完成,再接著執(zhí)行函數(shù)體內(nèi)后面的語句歧焦,看看下面這個(gè)例子:

async function func() {
  console.log('async function is running!');
  const num1 = await 200;
  console.log(`num1 is ${num1}`);
  const num2 = await num1+ 100;
  console.log(`num2 is ${num2}`);
  const num3 = await num2 + 100;
  console.log(`num3 is ${num3}`);
}

func();
console.log('run me before await!');
// async function is running!
// run me before await!
// num1 is 200
// num2 is 300
// num3 is 400

可以看出調(diào)用 async func 函數(shù)后移斩,它會立即執(zhí)行,首先輸出了'async function is running!'绢馍,接著遇到了await異步等待向瓷,函數(shù)返回,先執(zhí)行func()后面的同步任務(wù)舰涌,同步任務(wù)執(zhí)行完后猖任,接著await等待的位置繼續(xù)往下執(zhí)行。

值得注意的是瓷耙,await 后面的 Promise 對象不總是返回 resolved 狀態(tài)朱躺,只要一個(gè)await后面的Promise狀態(tài)變?yōu)閞ejected,整個(gè)async函數(shù)都會中斷執(zhí)行搁痛,為了保存錯(cuò)誤的位置和錯(cuò)誤信息长搀,我們需要用 try...catch 語句來封裝多個(gè)await過程,如下:

async function func() {
  try {
    const num1 = await 200;
    console.log(`num1 is ${num1}`);
    const num2 = await Promise.reject('num2 is wrong!');
    console.log(`num2 is ${num2}`);
    const num3 = await num2 + 100;
    console.log(`num3 is ${num3}`);
  } catch (error) {
    console.log(error);
  }
}

func();
// num1 is 200
// 出錯(cuò)了
// num2 is wrong!

如上所示鸡典,在num2處await得到了一個(gè)狀態(tài)為rejected的Promise對象源请,該錯(cuò)誤會被傳遞到catch語句中,這樣我們就可以定位錯(cuò)誤發(fā)生的位置彻况。

async/await比Promise強(qiáng)在哪兒谁尸?

接下來我們用async/await改寫一下Promise章節(jié)中關(guān)于timeAdder的一個(gè)例子,代碼如下:

function timeAdder(n) {
  return new Promise((resolved, rejected) => {
    setTimeout(() => {
      resolved(n + 200);
    }, n)
  })
}

async function func() {
  const time1 = await timeAdder(0);
  console.log(`time1 is ${time1}`);
  const time2 = await timeAdder(time1);
  console.log(`time3 is ${time2}`);
  const time3 = await timeAdder(time2);
  console.log(`time3 is ${time3}`);
}

func();
// time1 is 200
// time3 is 400
// time3 is 600

與之前長長的then鏈和then方法里的回調(diào)函數(shù)相比纽甘,這樣的寫法是不是更加清爽良蛮,更加符合編程習(xí)慣?

參考文章

https://segmentfault.com/a/1190000007535316
https://segmentfault.com/a/1190000006138882
https://www.zhihu.com/question/19732473/answer/20851256

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贷腕,一起剝皮案震驚了整個(gè)濱河市背镇,隨后出現(xiàn)的幾起案子咬展,更是在濱河造成了極大的恐慌泽裳,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件破婆,死亡現(xiàn)場離奇詭異涮总,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)祷舀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門瀑梗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烹笔,“玉大人,你說我怎么就攤上這事抛丽“埃” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵亿鲜,是天一觀的道長允蜈。 經(jīng)常有香客問我,道長蒿柳,這世上最難降的妖魔是什么饶套? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮垒探,結(jié)果婚禮上妓蛮,老公的妹妹穿的比我還像新娘。我一直安慰自己圾叼,他們只是感情好蛤克,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著夷蚊,像睡著了一般咖耘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撬码,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天儿倒,我揣著相機(jī)與錄音,去河邊找鬼呜笑。 笑死夫否,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的叫胁。 我是一名探鬼主播凰慈,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼驼鹅!你這毒婦竟也來了微谓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤输钩,失蹤者是張志新(化名)和其女友劉穎豺型,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體买乃,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡姻氨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了剪验。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肴焊。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡前联,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出娶眷,到底是詐尸還是另有隱情似嗤,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布届宠,位于F島的核電站双谆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏席揽。R本人自食惡果不足惜顽馋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望幌羞。 院中可真熱鬧寸谜,春花似錦、人聲如沸属桦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聂宾。三九已至果善,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間系谐,已是汗流浹背巾陕。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纪他,地道東北人鄙煤。 一個(gè)月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像茶袒,于是被迫代替她去往敵國和親梯刚。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351

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

  • Promise 對象 Promise 的含義 Promise 是異步編程的一種解決方案薪寓,比傳統(tǒng)的解決方案——回調(diào)函...
    neromous閱讀 8,703評論 1 56
  • Promise對象是一種解決異步問題的方法亡资,還有的解決方案是asyns 和 await (es7) 這么是目前的終...
    站在大神的肩膀上看世界閱讀 1,261評論 0 6
  • 目錄:Promise 的含義基本用法Promise.prototype.then()Promise.prototy...
    BluesCurry閱讀 1,490評論 0 8
  • “生活在這樣有意思的世界上,一個(gè)人即使悲傷向叉,也不會悲傷很久的锥腻,是不是?” 迷人的秋天到了植康,安妮在瑪麗拉的擔(dān)憂中...
    執(zhí)念sunshine閱讀 226評論 0 4
  • 這幾天萬老師在講解一篇文章《為什么佛學(xué)是真的》我覺得收獲頗多销睁。其中最精彩的是用正念法來提升我們的自控力供璧。這個(gè)概念刷...
    南方的雨中人閱讀 438評論 0 2