前端Tips#6 – 在 async iterator 上使用 for-await-of 語法糖

  • 2020 年 3 月 12 日
  • 筆記

題圖

影片講解

前往原文 前端Tips 專欄#6,點擊觀看

文字講解

本期主要是講解如何使用 for-await-of 語法糖進行非同步操作迭代,讓組織非同步操作的程式碼更加簡潔易讀。

1、場景簡述

以下程式碼中的 for...of 操作,列印順序 "2、3、4"(總共耗費時間 4s):

const delay = (time) => () => setTimeout(() => { console.log(time) }, time * 1000);    const delays = [delay(3), delay(2), delay(4)];    for (cur of delays) {      cur();  }

但我們想要以數組順序列印 「3、2、4」(總共耗時9s),請問該如何實現?

2、同步迭代器

以常見的數組列印為例,下述程式碼會依次列印出 "0、1":

for(const cur of [0, 1]){      console.log(cur);  }

那麼如何用 同步迭代器 實現上述同等輸出?

Iterator 是 ECMAScript 2015 引進的功能,它就是一個 function,只不過對這個 function 的形式有特殊的規定:

  1. 返回對象必須包含 next 屬性,該屬性也是 function
  2. next 函數返回值必須返回包含 donevalue 這兩個欄位的對象

有了 Iterator,就可以藉助 [Symbol.iterator] 構造出 可迭代對象(Iteratable)

// 返回一個可迭代對象,注意 [Symbol.iterator] 這個 key  const someIteratable = {      [Symbol.iterator]: someIterator  }

凡是可迭代對象就可以使用 for...of 語法,所以這是一種層層迭進的關係。

3、使用迭代器實現數組列印

知道了迭代器的概念後,就可以藉助迭代器實現上述的數組列印功能,首先自定義構造出 countIterator 迭代器

let count = 0;  function countIterator() {      // 返回一個迭代器對象,對象的屬性是一個 next 方法      return {          next: function () {              if (count < 2) {                  // 當沒有到達末尾時,返回當前值,並把索引加1                  return { value: count++, done: false };              }                // 到達末尾,done 屬性為 true              return { value: count, done: true };          }      };  }

然後創建出可迭代對象,由於該對象的行為和 [0,1] 這個數組類似,所以起名為 customArray

// 返回一個可迭代對象,注意 [Symbol.iterator] 這個 key  const customArray = {      [Symbol.iterator]: countIterator  }

最後給這個可迭代對象應用 for...of 即可,就能列印出 0、1 內容:

for (const cur of customArray) {      console.log(cur)  }

通過這個例子你就應該比較容易迭代器的理解,其實 JS 原生的StringArrayMapSet 等都是可迭代對象,因為它們的原型對象都有一個 Symbol.iterator 方法

4、非同步迭代器

理解了同步迭代器,那麼 非同步迭代器(Async Iterator)也就很容易理解了,它和同步迭代器的差別在於:

  1. 非同步迭代器必須返回 Promise 對象,且該 Promise 返回 { value, done } 格式對象
  2. 非同步可迭代對象(Async Iteratable)用 Symbol.asyncIterator 作為 key
  3. 非同步可迭代對象(Async Iteratable)可用 for-await-of 進行迭代

Async iterator 是 ECMAScript 2018 引進的

藉助非同步迭代器就可以實現本期開頭所講的功能,實現自定義的 delayIteraterable 可迭代對象,它使用 [Symbol.asyncIterator] 作為 key,其 value 就是非同步迭代器:

const promisify = func => (...args) =>      new Promise((resolve, reject) =>          func(...args, (err, result) => (err ? reject(err) : resolve(result)))      );    const delayIteraterable = {      [Symbol.asyncIterator]: () => {          return {              next: () => {                  const cur = promisify(delays.shift());                  return cur().then(res => {                      return {                          done: delays.length === 0,                          value: res                      }                  });              }          }      }  }

這裡用到的 promisify 函數,具體可參考前端 Tips – 第 5 期的內容講解。

然後直接搭配 for-await-of 語法糖使用,就能進行非同步迭代,按我們的要求依次輸出 「3、2、4」(總共耗時9s)

const execIt = async function () {      for await (const cur of delayIteraterable) {          console.log(cur);      }  }  execIt();

5、擴展:Generator & Async Generator

除了用迭代器生成 可迭代對象 外,還能用 Generator(生成器)生成 可迭代對象,而且一般來講程式碼實現也更為緊湊。

由於時間關係就不展開了,感興趣的可閱讀文末的參考文章自行學習。本期的例子也提供了 generator 的版本可供參考,鏈接:https://github.com/boycgit/fe-program-tips/blob/master/src/6-async-iterator/async-yield.js

6、參考文檔

附:如何獲取往期 「前端Tips」 列表

有兩種方式獲取歷史 tips:

① 在公眾號內回「tips」 + 「年份」 + 「A(或者B)」 獲取半年度 tips。例如:回復 「tips2020A」 即可獲取 2020 年上半年 tips 列表

② 前往網站:https://boycgit.github.io/fe-program-tips ,裡面提供了搜索功能

歡迎大家關注我的知識專欄,更多內容等你來挖掘

「可在微信內搜索 「JSCON簡時空」或 「iJSCON」 關注」