ES6 的異步遍歷器

2020-10-20 14:43 更新

1. 同步遍歷器的問題

《遍歷器》一章說過,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)生。

2. 異步遍歷的接口

異步遍歷器的最大的語法特點,就是調(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();

3. 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 對象被 reject , for 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 ###');
}

4. 異步 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ù) func 。map 的作用是將 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ù)拋出錯誤,會導(dǎo)致 Promise 對象的狀態(tài)變?yōu)?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)?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)鍵字。

5. 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
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號