async/await的深究

我們都知道async/awaitGenerator函數(shù)的語(yǔ)法糖,為了更加深刻的了解async/await的原理,我們先來(lái)研究一下Generator的相關(guān)的知識(shí)抒痒。

基本概念

Generator:可以被認(rèn)為是一個(gè)狀態(tài)機(jī)杉允,其內(nèi)部封裝了多個(gè)內(nèi)部的狀態(tài)膳犹。執(zhí)行該函數(shù)會(huì)返回一個(gè)遍歷器對(duì)象终蒂,也就是蜂林,Generator函數(shù)除了狀態(tài),還是一個(gè)遍歷器對(duì)象生成函數(shù)后豫,返回的遍歷器對(duì)象悉尾,可以依次遍歷Generator內(nèi)部的每一個(gè)狀態(tài)突那。

函數(shù)特征:

  • function關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào)
  • 函數(shù)內(nèi)部使用yield表達(dá)式挫酿,定義不同的內(nèi)部狀態(tài)
function* foo(){
  yield 'hello';
  yield 'word';
  return 'ending';
}
var hw = foo();

上面的代碼定義了一個(gè)Generator函數(shù),內(nèi)部由兩個(gè)yield表達(dá)式(helloworld),即將執(zhí)行的函數(shù)有三個(gè)狀態(tài):hello,worldreturn語(yǔ)句愕难。

Generator函數(shù)調(diào)用和普通函數(shù)的調(diào)用是一樣的早龟,也是在函數(shù)名后面加上一對(duì)圓括號(hào)。但是調(diào)用Generator調(diào)用之后并不執(zhí)行猫缭,返回的也不是函數(shù)運(yùn)行結(jié)果葱弟,而是指向內(nèi)部狀態(tài)的指針對(duì)象,也就是上面說(shuō)的遍歷器對(duì)象猜丹。下一步必須調(diào)用遍歷對(duì)象的next方法芝加,讓指針指向下一個(gè)狀態(tài),也就是說(shuō)射窒,每一次調(diào)用next方法藏杖,內(nèi)部指針就會(huì)從函數(shù)頭或是上一次停下來(lái)的地方開始執(zhí)行将塑,直到遇到下一個(gè)yield表達(dá)式為止。換言之蝌麸,Generator 函數(shù)是分段執(zhí)行的点寥,yield表達(dá)式是暫停執(zhí)行的標(biāo)記,而next方法可以恢復(fù)執(zhí)行来吩。

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

done屬性的值false敢辩,表示遍歷還沒有結(jié)束。否則表示遍歷已經(jīng)結(jié)束啦弟疆。

第四次調(diào)用戚长,此時(shí) Generator 函數(shù)已經(jīng)運(yùn)行完畢,next方法返回對(duì)象的value屬性為undefined兽间,done屬性為true历葛。以后再調(diào)用next方法,返回的都是這個(gè)值嘀略。

在這個(gè)過(guò)程中恤溶,最重要的是next的方法的運(yùn)行邏輯夫植,下面我們對(duì)這個(gè)過(guò)程進(jìn)行一下梳理:

  • 遇到yield表達(dá)式杀饵,先暫停后面的操作,將緊跟著yield后面的表達(dá)式的值箫柳,作為返回對(duì)象的value屬性值進(jìn)行返回
  • 下一次調(diào)用next方法的時(shí)候讼育,再繼續(xù)往下執(zhí)行帐姻,知道遇到下一個(gè)yield表達(dá)式。
  • 如果沒有遇到新的yield表達(dá)式奶段,就一直運(yùn)行直到函數(shù)結(jié)束饥瓷,直到return語(yǔ)句為止,并將return語(yǔ)句后面的表達(dá)式的值痹籍,作為返回的對(duì)象的value屬性值呢铆。
  • 如果沒有遇到return語(yǔ)句,就會(huì)返回對(duì)象的value屬性值為undefined蹲缠。

yield表達(dá)式與return語(yǔ)句既有相似之處棺克,也有區(qū)別。相似之處在于线定,都能返回緊跟在語(yǔ)句后面的那個(gè)表達(dá)式的值娜谊。區(qū)別在于每次遇到yield,函數(shù)暫停執(zhí)行斤讥,下一次再?gòu)脑撐恢美^續(xù)向后執(zhí)行纱皆,而return語(yǔ)句不具備位置記憶的功能。一個(gè)函數(shù)里面,只能執(zhí)行一次(或者說(shuō)一個(gè))return語(yǔ)句派草,但是可以執(zhí)行多次(或者說(shuō)多個(gè))yield表達(dá)式撑帖。

異步操作的同步化表達(dá)

我們舉一個(gè)例子來(lái)實(shí)現(xiàn)將異步的操作同步化:過(guò) Generator 函數(shù)部署 Ajax 操作,可以用同步的方式表達(dá)澳眷。

function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}

var it = main();
it.next();

makeAjaxCall函數(shù)中的next方法胡嘿,必須加上response參數(shù),因?yàn)?code>yield表達(dá)式钳踊,本身是沒有值的衷敌,總是等于undefined

Generator 函數(shù)的異步應(yīng)用

ES6誕生以來(lái)主要實(shí)現(xiàn)異步編程的方法有如下的四種:

  • 回調(diào)函數(shù) -- 回調(diào)地獄
  • 事件監(jiān)聽
  • 發(fā)布訂閱
  • Promise 對(duì)象 -- 代碼冗余

回調(diào)函數(shù)

JavaScript語(yǔ)言對(duì)異步編程的實(shí)現(xiàn)拓瞪,就是回調(diào)函數(shù)缴罗,回調(diào)函數(shù),就是把任務(wù)的第二階段單獨(dú)寫在一個(gè)函數(shù)中祭埂,等待重新執(zhí)行這個(gè)任務(wù)的時(shí)候面氓,就直接調(diào)用這個(gè)函數(shù),回調(diào)函數(shù)的英文名字就是callback,直接翻譯過(guò)來(lái)就是"重新調(diào)用"

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});

readFile函數(shù)的第三個(gè)參數(shù)蛆橡,就是回調(diào)函數(shù)舌界,也就是任務(wù)的第二段。等到操作系統(tǒng)返回了/etc/passwd這個(gè)文件以后泰演,回調(diào)函數(shù)才會(huì)執(zhí)行呻拌。

Promise

回調(diào)函數(shù)本身并沒有問題,它的問題出現(xiàn)在多個(gè)回調(diào)函數(shù)嵌套睦焕。假定讀取A文件之后藐握,再讀取B文件,代碼如下垃喊。

fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
});

會(huì)出現(xiàn)多重回調(diào)的情況猾普,進(jìn)而造成代碼的可讀性變差,形成回調(diào)地獄('callback hell')

Promise對(duì)象就是為了解決這個(gè)問題而提出的本谜。它不是新的語(yǔ)法功能初家,而是一種新的寫法,允許將回調(diào)函數(shù)的嵌套耕突,改成鏈?zhǔn)秸{(diào)用笤成。采用 Promise评架,連續(xù)讀取多個(gè)文件眷茁,

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

Promise的最大問題是代碼冗余,原來(lái)的任務(wù)被Promise包裝了一下纵诞,不管什么操作上祈,一眼看去都是一堆then,原來(lái)的語(yǔ)義變得很不清楚。

Generator 函數(shù)

協(xié)程有點(diǎn)像函數(shù)登刺,又有點(diǎn)像線程籽腕。它的運(yùn)行流程大致如下。

  • 協(xié)程A開始執(zhí)行纸俭。
  • 協(xié)程A執(zhí)行到一半皇耗,進(jìn)入暫停,執(zhí)行權(quán)轉(zhuǎn)移到協(xié)程B揍很。
  • (一段時(shí)間后)協(xié)程B交還執(zhí)行權(quán)郎楼。
  • 協(xié)程A恢復(fù)執(zhí)行。

Generator 函數(shù)是協(xié)程在ES6 的實(shí)現(xiàn)窒悔,最大特點(diǎn)就是可以交出函數(shù)的執(zhí)行權(quán)呜袁,下面看看如何使用 Generator函數(shù),執(zhí)行一個(gè)真實(shí)的異步任務(wù)简珠。

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

async/await

前文有一個(gè)Generator 函數(shù)阶界,依次讀取兩個(gè)文件。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

上面代碼的函數(shù)gen可以寫成async函數(shù)聋庵,就是下面這樣膘融。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async函數(shù)就是將Generator函數(shù)的星號(hào)(*)替換成async,將yield替換成await
async函數(shù)對(duì) Generator函數(shù)的改進(jìn)

  • 內(nèi)置執(zhí)行器祭玉。
    async函數(shù)自帶執(zhí)行器托启。也就是說(shuō),async函數(shù)的執(zhí)行攘宙,與普通函數(shù)一模一樣屯耸,只要一行。
  • 更好的語(yǔ)義
    asyncawait蹭劈,比起星號(hào)和yield疗绣,語(yǔ)義更清楚了。async表示函數(shù)里有異步操作铺韧,await表示緊跟在后面的表達(dá)式需要等待結(jié)果多矮。
  • 更廣的適用性。
    yield命令后面只能是 Thunk 函數(shù)或 Promise對(duì)象哈打,而async函數(shù)的await命令后面塔逃,可以是Promise對(duì)象和原始類型的值(數(shù)值、字符串和布爾值料仗,但這時(shí)會(huì)自動(dòng)轉(zhuǎn)成立即resolvedPromise對(duì)象)湾盗。
  • 返回值是Promise
    async函數(shù)的返回值是 Promise 對(duì)象

async 函數(shù)的實(shí)現(xiàn)原理

async 函數(shù)的實(shí)現(xiàn)原理立轧,就是將 Generator 函數(shù)和自動(dòng)執(zhí)行器格粪,包裝在一個(gè)函數(shù)里躏吊。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

所有的async函數(shù)都可以寫成上面的第二種形式,其中的spawn函數(shù)就是自動(dòng)執(zhí)行器,我們看一下spawn的執(zhí)行過(guò)程:

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        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); });
  });
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帐萎,一起剝皮案震驚了整個(gè)濱河市比伏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌疆导,老刑警劉巖赁项,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異澈段,居然都是意外死亡肤舞,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門均蜜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)李剖,“玉大人,你說(shuō)我怎么就攤上這事囤耳「菟常” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵充择,是天一觀的道長(zhǎng)德玫。 經(jīng)常有香客問我,道長(zhǎng)椎麦,這世上最難降的妖魔是什么宰僧? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮观挎,結(jié)果婚禮上琴儿,老公的妹妹穿的比我還像新娘。我一直安慰自己嘁捷,他們只是感情好造成,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著雄嚣,像睡著了一般晒屎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缓升,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天鼓鲁,我揣著相機(jī)與錄音,去河邊找鬼港谊。 笑死骇吭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的封锉。 我是一名探鬼主播绵跷,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼成福!你這毒婦竟也來(lái)了碾局?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤奴艾,失蹤者是張志新(化名)和其女友劉穎净当,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蕴潦,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡像啼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了潭苞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忽冻。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖此疹,靈堂內(nèi)的尸體忽然破棺而出僧诚,到底是詐尸還是另有隱情,我是刑警寧澤蝗碎,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布湖笨,位于F島的核電站,受9級(jí)特大地震影響蹦骑,放射性物質(zhì)發(fā)生泄漏慈省。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一眠菇、第九天 我趴在偏房一處隱蔽的房頂上張望边败。 院中可真熱鬧,春花似錦捎废、人聲如沸放闺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)怖侦。三九已至,卻和暖如春谜叹,著一層夾襖步出監(jiān)牢的瞬間匾寝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工荷腊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留艳悔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓女仰,卻偏偏與公主長(zhǎng)得像猜年,于是被迫代替她去往敵國(guó)和親抡锈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354