27. 異步遍歷器

僅為方便個人查詢使用
來源:http://es6.ruanyifeng.com/#docs/async-iterator

同步遍歷器的問題

《遍歷器》一章說過域那,Iterator 接口是一種數(shù)據(jù)遍歷的協(xié)議捻悯,只要調(diào)用遍歷器對象的next方法蝴悉,就會得到一個對象,表示當前遍歷指針所在的那個位置的信息膨俐。next方法返回的對象的結(jié)構(gòu)是{value, done},其中value表示當前的數(shù)據(jù)的值,done是一個布爾值丑孩,表示遍歷是否結(jié)束。

function idMaker() {
  let index = 0;

  return {
    next: function() {
      return { value: index++, done: false };
    }
  };
}

const it = idMaker();

it.next().value // 0
it.next().value // 1
it.next().value // 2
// ...

上面代碼中灭贷,變量it是一個遍歷器(iterator)温学。每次調(diào)用it.next()方法,就返回一個對象甚疟,表示當前遍歷位置的信息仗岖。

這里隱含著一個規(guī)定,it.next()方法必須是同步的览妖,只要調(diào)用就必須立刻返回值轧拄。也就是說,一旦執(zhí)行it.next()方法黄痪,就必須同步地得到valuedone這兩個屬性紧帕。如果遍歷指針正好指向同步操作,當然沒有問題桅打,但對于異步操作是嗜,就不太合適了。

function idMaker() {
  let index = 0;

  return {
    next: function() {
      return new Promise(function (resolve, reject) {
        setTimeout(() => {
          resolve({ value: index++, done: false });
        }, 1000);
      });
    }
  };
}

上面代碼中挺尾,next()方法返回的是一個 Promise 對象鹅搪,這樣就不行,不符合 Iterator 協(xié)議遭铺,只要代碼里面包含異步操作都不行丽柿。也就是說,Iterator 協(xié)議里面next()方法只能包含同步操作魂挂。

目前的解決方法是甫题,將異步操作包裝成 Thunk 函數(shù)或者 Promise 對象,即next()方法返回值的value屬性是一個 Thunk 函數(shù)或者 Promise 對象涂召,等待以后返回真正的值坠非,而done屬性則還是同步產(chǎn)生的。

function idMaker() {
  let index = 0;

  return {
    next: function() {
      return {
        value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)),
        done: false
      };
    }
  };
}

const it = idMaker();

it.next().value.then(o => console.log(o)) // 1
it.next().value.then(o => console.log(o)) // 2
it.next().value.then(o => console.log(o)) // 3
// ...

上面代碼中果正,value屬性的返回值是一個 Promise 對象炎码,用來放置異步操作盟迟。但是這樣寫很麻煩,不太符合直覺潦闲,語義也比較繞攒菠。

ES2018 引入了“異步遍歷器”(Async Iterator),為異步操作提供原生的遍歷器接口歉闰,即valuedone這兩個屬性都是異步產(chǎn)生辖众。

異步遍歷的接口

異步遍歷器的最大的語法特點,就是調(diào)用遍歷器的next方法新娜,返回的是一個 Promise 對象赵辕。

asyncIterator
  .next()
  .then(
    ({ value, done }) => /* ... */
  );

上面代碼中,asyncIterator是一個異步遍歷器概龄,調(diào)用next方法以后还惠,返回一個 Promise 對象。因此私杜,可以使用then方法指定蚕键,這個 Promise 對象的狀態(tài)變?yōu)?code>resolve以后的回調(diào)函數(shù)∷ゴ猓回調(diào)函數(shù)的參數(shù)锣光,則是一個具有valuedone兩個屬性的對象,這個跟同步遍歷器是一樣的铝耻。

我們知道誊爹,一個對象的同步遍歷器的接口,部署在Symbol.iterator屬性上面瓢捉。同樣地频丘,對象的異步遍歷器接口,部署在Symbol.asyncIterator屬性上面泡态。不管是什么樣的對象搂漠,只要它的Symbol.asyncIterator屬性有值,就表示應(yīng)該對它進行異步遍歷某弦。

下面是一個異步遍歷器的例子桐汤。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();

asyncIterator
.next()
.then(iterResult1 => {
  console.log(iterResult1); // { value: 'a', done: false }
  return asyncIterator.next();
})
.then(iterResult2 => {
  console.log(iterResult2); // { value: 'b', done: false }
  return asyncIterator.next();
})
.then(iterResult3 => {
  console.log(iterResult3); // { value: undefined, done: true }
});

上面代碼中,異步遍歷器其實返回了兩次值靶壮。第一次調(diào)用的時候怔毛,返回一個 Promise 對象;等到 Promise 對象resolve了腾降,再返回一個表示當前數(shù)據(jù)成員信息的對象馆截。這就是說,異步遍歷器與同步遍歷器最終行為是一致的,只是會先返回 Promise 對象蜡娶,作為中介。

由于異步遍歷器的next方法映穗,返回的是一個 Promise 對象窖张。因此,可以把它放在await命令后面蚁滋。

async function f() {
  const asyncIterable = createAsyncIterable(['a', 'b']);
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  console.log(await asyncIterator.next());
  // { value: 'a', done: false }
  console.log(await asyncIterator.next());
  // { value: 'b', done: false }
  console.log(await asyncIterator.next());
  // { value: undefined, done: true }
}

上面代碼中宿接,next方法用await處理以后,就不必使用then方法了辕录。整個流程已經(jīng)很接近同步處理了睦霎。

注意,異步遍歷器的next方法是可以連續(xù)調(diào)用的走诞,不必等到上一步產(chǎn)生的 Promise 對象resolve以后再調(diào)用副女。這種情況下,next方法會累積起來蚣旱,自動按照每一步的順序運行下去碑幅。下面是一個例子,把所有的next方法放在Promise.all方法里面塞绿。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
  asyncIterator.next(), asyncIterator.next()
]);

console.log(v1, v2); // a b

另一種用法是一次性調(diào)用所有的next方法沟涨,然后await最后一步操作。

async function runner() {
  const writer = openFile('someFile.txt');
  writer.next('hello');
  writer.next('world');
  await writer.return();
}

runner();

for await...of

前面介紹過异吻,for...of循環(huán)用于遍歷同步的 Iterator 接口裹赴。新引入的for await...of循環(huán),則是用于遍歷異步的 Iterator 接口诀浪。

async function f() {
  for await (const x of createAsyncIterable(['a', 'b'])) {
    console.log(x);
  }
}
// a
// b

上面代碼中棋返,createAsyncIterable()返回一個擁有異步遍歷器接口的對象,for...of循環(huán)自動調(diào)用這個對象的異步遍歷器的next方法笋妥,會得到一個 Promise 對象懊昨。await用來處理這個 Promise 對象,一旦resolve春宣,就把得到的值(x)傳入for...of的循環(huán)體酵颁。

for await...of循環(huán)的一個用途,是部署了 asyncIterable 操作的異步接口月帝,可以直接放入這個循環(huán)躏惋。

let body = '';

async function f() {
  for await(const data of req) body += data;
  const parsed = JSON.parse(body);
  console.log('got', parsed);
}

上面代碼中,req是一個 asyncIterable 對象嚷辅,用來異步讀取數(shù)據(jù)簿姨。可以看到,使用for await...of循環(huán)以后扁位,代碼會非常簡潔准潭。

如果next方法返回的 Promise 對象被rejectfor await...of就會報錯域仇,要用try...catch捕捉刑然。

async function () {
  try {
    for await (const x of createRejectingIterable()) {
      console.log(x);
    }
  } catch (e) {
    console.error(e);
  }
}

注意,for await...of循環(huán)也可以用于同步遍歷器暇务。

(async function () {
  for await (const x of ['a', 'b']) {
    console.log(x);
  }
})();
// a
// b

Node v10 支持異步遍歷器泼掠,Stream 就部署了這個接口。下面是讀取文件的傳統(tǒng)寫法與異步遍歷器寫法的差異垦细。

// 傳統(tǒng)寫法
function main(inputFilePath) {
  const readStream = fs.createReadStream(
    inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 }
  );
  readStream.on('data', (chunk) => {
    console.log('>>> '+chunk);
  });
  readStream.on('end', () => {
    console.log('### DONE ###');
  });
}

// 異步遍歷器寫法
async function main(inputFilePath) {
  const readStream = fs.createReadStream(
    inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 }
  );

  for await (const chunk of readStream) {
    console.log('>>> '+chunk);
  }
  console.log('### DONE ###');
}

異步 Generator 函數(shù)

就像 Generator 函數(shù)返回一個同步遍歷器對象一樣择镇,異步 Generator 函數(shù)的作用,是返回一個異步遍歷器對象括改。

在語法上腻豌,異步 Generator 函數(shù)就是async函數(shù)與 Generator 函數(shù)的結(jié)合。

async function* gen() {
  yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }

上面代碼中叹谁,gen是一個異步 Generator 函數(shù)饲梭,執(zhí)行后返回一個異步 Iterator 對象。對該對象調(diào)用next方法焰檩,返回一個 Promise 對象憔涉。

異步遍歷器的設(shè)計目的之一,就是 Generator 函數(shù)處理同步操作和異步操作時析苫,能夠使用同一套接口兜叨。

// 同步 Generator 函數(shù)
function* map(iterable, func) {
  const iter = iterable[Symbol.iterator]();
  while (true) {
    const {value, done} = iter.next();
    if (done) break;
    yield func(value);
  }
}

// 異步 Generator 函數(shù)
async function* map(iterable, func) {
  const iter = iterable[Symbol.asyncIterator]();
  while (true) {
    const {value, done} = await iter.next();
    if (done) break;
    yield func(value);
  }
}

上面代碼中,map是一個 Generator 函數(shù)衩侥,第一個參數(shù)是可遍歷對象iterable国旷,第二個參數(shù)是一個回調(diào)函數(shù)funcmap的作用是將iterable每一步返回的值茫死,使用func進行處理跪但。上面有兩個版本的map,前一個處理同步遍歷器峦萎,后一個處理異步遍歷器屡久,可以看到兩個版本的寫法基本上是一致的。

下面是另一個異步 Generator 函數(shù)的例子爱榔。

async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}

上面代碼中被环,異步操作前面使用await關(guān)鍵字標明,即await后面的操作详幽,應(yīng)該返回 Promise 對象筛欢。凡是使用yield關(guān)鍵字的地方浸锨,就是next方法停下來的地方,它后面的表達式的值(即await file.readLine()的值)版姑,會作為next()返回對象的value屬性柱搜,這一點是與同步 Generator 函數(shù)一致的。

異步 Generator 函數(shù)內(nèi)部剥险,能夠同時使用awaityield命令冯凹。可以這樣理解炒嘲,await命令用于將外部操作產(chǎn)生的值輸入函數(shù)內(nèi)部,yield命令用于將函數(shù)內(nèi)部的值輸出匈庭。

上面代碼定義的異步 Generator 函數(shù)的用法如下夫凸。

(async function () {
  for await (const line of readLines(filePath)) {
    console.log(line);
  }
})()

異步 Generator 函數(shù)可以與for await...of循環(huán)結(jié)合起來使用。

async function* prefixLines(asyncIterable) {
  for await (const line of asyncIterable) {
    yield '> ' + line;
  }
}

異步 Generator 函數(shù)的返回值是一個異步 Iterator阱持,即每次調(diào)用它的next方法夭拌,會返回一個 Promise 對象,也就是說衷咽,跟在yield命令后面的鸽扁,應(yīng)該是一個 Promise 對象。如果像上面那個例子那樣镶骗,yield命令后面是一個字符串桶现,會被自動包裝成一個 Promise 對象。

function fetchRandom() {
  const url = 'https://www.random.org/decimal-fractions/'
    + '?num=1&dec=10&col=1&format=plain&rnd=new';
  return fetch(url);
}

async function* asyncGenerator() {
  console.log('Start');
  const result = await fetchRandom(); // (A)
  yield 'Result: ' + await result.text(); // (B)
  console.log('Done');
}

const ag = asyncGenerator();
ag.next().then(({value, done}) => {
  console.log(value);
})

上面代碼中鼎姊,agasyncGenerator函數(shù)返回的異步遍歷器對象骡和。調(diào)用ag.next()以后,上面代碼的執(zhí)行順序如下相寇。

  1. ag.next()立刻返回一個 Promise 對象慰于。
  2. asyncGenerator函數(shù)開始執(zhí)行,打印出Start唤衫。
  3. await命令返回一個 Promise 對象婆赠,asyncGenerator函數(shù)停在這里。
  4. A 處變成 fulfilled 狀態(tài)佳励,產(chǎn)生的值放入result變量休里,asyncGenerator函數(shù)繼續(xù)往下執(zhí)行。
  5. 函數(shù)在 B 處的yield暫停執(zhí)行植兰,一旦yield命令取到值份帐,ag.next()返回的那個 Promise 對象變成 fulfilled 狀態(tài)。
  6. ag.next()后面的then方法指定的回調(diào)函數(shù)開始執(zhí)行楣导。該回調(diào)函數(shù)的參數(shù)是一個對象{value, done}废境,其中value的值是yield命令后面的那個表達式的值,done的值是false

A 和 B 兩行的作用類似于下面的代碼噩凹。

return new Promise((resolve, reject) => {
  fetchRandom()
  .then(result => result.text())
  .then(result => {
     resolve({
       value: 'Result: ' + result,
       done: false,
     });
  });
});

如果異步 Generator 函數(shù)拋出錯誤巴元,會導致 Promise 對象的狀態(tài)變?yōu)?code>reject,然后拋出的錯誤被catch方法捕獲驮宴。

async function* asyncGenerator() {
  throw new Error('Problem!');
}

asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!

注意逮刨,普通的 async 函數(shù)返回的是一個 Promise 對象,而異步 Generator 函數(shù)返回的是一個異步 Iterator 對象堵泽⌒藜海可以這樣理解,async 函數(shù)和異步 Generator 函數(shù)迎罗,是封裝異步操作的兩種方法睬愤,都用來達到同一種目的。區(qū)別在于纹安,前者自帶執(zhí)行器尤辱,后者通過for await...of執(zhí)行,或者自己編寫執(zhí)行器厢岂。下面就是一個異步 Generator 函數(shù)的執(zhí)行器光督。

async function takeAsync(asyncIterable, count = Infinity) {
  const result = [];
  const iterator = asyncIterable[Symbol.asyncIterator]();
  while (result.length < count) {
    const {value, done} = await iterator.next();
    if (done) break;
    result.push(value);
  }
  return result;
}

上面代碼中,異步 Generator 函數(shù)產(chǎn)生的異步遍歷器塔粒,會通過while循環(huán)自動執(zhí)行结借,每當await iterator.next()完成,就會進入下一輪循環(huán)窗怒。一旦done屬性變?yōu)?code>true映跟,就會跳出循環(huán),異步遍歷器執(zhí)行結(jié)束扬虚。

下面是這個自動執(zhí)行器的一個使用實例努隙。

async function f() {
  async function* gen() {
    yield 'a';
    yield 'b';
    yield 'c';
  }

  return await takeAsync(gen());
}

f().then(function (result) {
  console.log(result); // ['a', 'b', 'c']
})

異步 Generator 函數(shù)出現(xiàn)以后,JavaScript 就有了四種函數(shù)形式:普通函數(shù)辜昵、async 函數(shù)荸镊、Generator 函數(shù)和異步 Generator 函數(shù)。請注意區(qū)分每種函數(shù)的不同之處堪置」妫基本上,如果是一系列按照順序執(zhí)行的異步操作(比如讀取文件舀锨,然后寫入新內(nèi)容岭洲,再存入硬盤),可以使用 async 函數(shù)坎匿;如果是一系列產(chǎn)生相同數(shù)據(jù)結(jié)構(gòu)的異步操作(比如一行一行讀取文件)盾剩,可以使用異步 Generator 函數(shù)雷激。

異步 Generator 函數(shù)也可以通過next方法的參數(shù),接收外部傳入的數(shù)據(jù)告私。

const writer = openFile('someFile.txt');
writer.next('hello'); // 立即執(zhí)行
writer.next('world'); // 立即執(zhí)行
await writer.return(); // 等待寫入結(jié)束

上面代碼中屎暇,openFile是一個異步 Generator 函數(shù)。next方法的參數(shù)驻粟,向該函數(shù)內(nèi)部的操作傳入數(shù)據(jù)根悼。每次next方法都是同步執(zhí)行的,最后的await命令用于等待整個寫入操作結(jié)束蜀撑。

最后挤巡,同步的數(shù)據(jù)結(jié)構(gòu),也可以使用異步 Generator 函數(shù)酷麦。

async function* createAsyncIterable(syncIterable) {
  for (const elem of syncIterable) {
    yield elem;
  }
}

上面代碼中玄柏,由于沒有異步操作,所以也就沒有使用await關(guān)鍵字贴铜。

yield* 語句

yield*語句也可以跟一個異步遍歷器。

async function* gen1() {
  yield 'a';
  yield 'b';
  return 2;
}

async function* gen2() {
  // result 最終會等于 2
  const result = yield* gen1();
}

上面代碼中瀑晒,gen2函數(shù)里面的result變量绍坝,最后的值是2

與同步 Generator 函數(shù)一樣苔悦,for await...of循環(huán)會展開yield*轩褐。

(async function () {
  for await (const x of gen2()) {
    console.log(x);
  }
})();
// a
// b
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市玖详,隨后出現(xiàn)的幾起案子把介,更是在濱河造成了極大的恐慌,老刑警劉巖蟋座,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拗踢,死亡現(xiàn)場離奇詭異,居然都是意外死亡向臀,警方通過查閱死者的電腦和手機巢墅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來券膀,“玉大人君纫,你說我怎么就攤上這事∏郾颍” “怎么了蓄髓?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舒帮。 經(jīng)常有香客問我会喝,道長陡叠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任好乐,我火速辦了婚禮匾竿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蔚万。我一直安慰自己岭妖,他們只是感情好,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布反璃。 她就那樣靜靜地躺著昵慌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淮蜈。 梳的紋絲不亂的頭發(fā)上斋攀,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機與錄音梧田,去河邊找鬼淳蔼。 笑死,一個胖子當著我的面吹牛裁眯,可吹牛的內(nèi)容都是我干的鹉梨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼穿稳,長吁一口氣:“原來是場噩夢啊……” “哼存皂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逢艘,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤旦袋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后它改,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疤孕,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年央拖,在試婚紗的時候發(fā)現(xiàn)自己被綠了胰柑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡爬泥,死狀恐怖柬讨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情袍啡,我是刑警寧澤踩官,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站境输,受9級特大地震影響蔗牡,放射性物質(zhì)發(fā)生泄漏颖系。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一辩越、第九天 我趴在偏房一處隱蔽的房頂上張望嘁扼。 院中可真熱鬧,春花似錦黔攒、人聲如沸趁啸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽不傅。三九已至,卻和暖如春赏胚,著一層夾襖步出監(jiān)牢的瞬間访娶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工觉阅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留崖疤,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓典勇,卻偏偏與公主長得像戳晌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子痴柔,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355