Javascript 異步迭代和 generator

2023-02-17 10:53 更新

異步迭代允許我們對(duì)按需通過(guò)異步請(qǐng)求而得到的數(shù)據(jù)進(jìn)行迭代。例如,我們通過(guò)網(wǎng)絡(luò)分段(chunk-by-chunk)下載數(shù)據(jù)時(shí)。異步生成器(generator)使這一步驟更加方便。

首先,讓我們來(lái)看一個(gè)簡(jiǎn)單的示例以掌握語(yǔ)法,然后再看一個(gè)實(shí)際用例。

回顧可迭代對(duì)象

讓我們回顧一下可迭代對(duì)象的相關(guān)內(nèi)容。

假設(shè)我們有一個(gè)對(duì)象,例如下面的 range

let range = {
  from: 1,
  to: 5
};

我們想對(duì)它使用 for..of 循環(huán),例如 for(value of range),來(lái)獲取從 1 到 5 的值。

換句話(huà)說(shuō),我們想向?qū)ο?nbsp;range 添加 迭代能力。

這可以通過(guò)使用一個(gè)名為 Symbol.iterator 的特殊方法來(lái)實(shí)現(xiàn):

  • 當(dāng)循環(huán)開(kāi)始時(shí),該方法被 ?for..of? 結(jié)構(gòu)調(diào)用,并且它應(yīng)該返回一個(gè)帶有 ?next? 方法的對(duì)象。
  • 對(duì)于每次迭代,都會(huì)為下一個(gè)值調(diào)用 ?next()? 方法。
  • ?next()? 方法應(yīng)該以 ?{done: true/false, value:<loop value>}? 的格式返回一個(gè)值,其中 ?done:true? 表示循環(huán)結(jié)束。

這是可迭代的 range 的一個(gè)實(shí)現(xiàn):

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() { // 在 for..of 循環(huán)開(kāi)始時(shí)被調(diào)用一次
    return {
      current: this.from,
      last: this.to,

      next() { // 每次迭代時(shí)都會(huì)被調(diào)用,來(lái)獲取下一個(gè)值
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for(let value of range) {
  alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}

如果有任何不清楚的,你可以閱讀 Iterable object(可迭代對(duì)象) 一章,其中詳細(xì)講解了關(guān)于常規(guī)迭代器(iterator)的所有內(nèi)容。

異步可迭代對(duì)象

當(dāng)值是以異步的形式出現(xiàn)時(shí),例如在 setTimeout 或者另一種延遲之后,就需要異步迭代。

最常見(jiàn)的場(chǎng)景是,對(duì)象需要發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求以傳遞下一個(gè)值,稍后我們將看到一個(gè)它的真實(shí)示例。

要使對(duì)象異步迭代:

  1. 使用 ?Symbol.asyncIterator? 取代 ?Symbol.iterator?。
  2. ?next()? 方法應(yīng)該返回一個(gè) ?promise?(帶有下一個(gè)值,并且狀態(tài)為 ?fulfilled?)。
    • 關(guān)鍵字 ?async? 可以實(shí)現(xiàn)這一點(diǎn),我們可以簡(jiǎn)單地使用 ?async next()?。
  3. 我們應(yīng)該使用 ?for await (let item of iterable)? 循環(huán)來(lái)迭代這樣的對(duì)象。
    • 注意關(guān)鍵字 ?await?。

作為開(kāi)始的示例,讓我們創(chuàng)建一個(gè)可迭代的 range 對(duì)象,與前面的那個(gè)類(lèi)似,不過(guò)現(xiàn)在它將異步地每秒返回一個(gè)值。

我們需要做的就是對(duì)上面代碼中的部分代碼進(jìn)行替換:

let range = {
  from: 1,
  to: 5,

  [Symbol.asyncIterator]() { // (1)
    return {
      current: this.from,
      last: this.to,

      async next() { // (2)

        // 注意:我們可以在 async next 內(nèi)部使用 "await"
        await new Promise(resolve => setTimeout(resolve, 1000)); // (3)

        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {

  for await (let value of range) { // (4)
    alert(value); // 1,2,3,4,5
  }

})()

正如我們所看到的,其結(jié)構(gòu)與常規(guī)的 iterator 類(lèi)似:

  1. 為了使一個(gè)對(duì)象可以異步迭代,它必須具有方法 ?Symbol.asyncIterator ?(1)??。
  2. 這個(gè)方法必須返回一個(gè)帶有 ?next()? 方法的對(duì)象,?next()? 方法會(huì)返回一個(gè) promise ?(2)?。
  3. 這個(gè) ?next()? 方法可以不是 ?async? 的,它可以是一個(gè)返回值是一個(gè) ?promise? 的常規(guī)的方法,但是使用 ?async? 關(guān)鍵字可以允許我們?cè)诜椒▋?nèi)部使用 ?await?,所以會(huì)更加方便。這里我們只是用于延遲 1 秒的操作 ?(3)?。
  4. 我們使用 ?for await(let value of range)? ?(4)? 來(lái)進(jìn)行迭代,也就是在 ?for? 后面添加 ?await?。它會(huì)調(diào)用一次 ?range[Symbol.asyncIterator]()? 方法一次,然后調(diào)用它的 ?next()? 方法獲取值。

這是一個(gè)對(duì)比 Iterator 和異步 iterator 之間差異的表格:

Iterator 異步 iterator
提供 iterator 的對(duì)象方法 Symbol.iterator Symbol.asyncIterator
next() 返回的值是 任意值 Promise
要進(jìn)行循環(huán),使用 for..of for await..of

Spread 語(yǔ)法 ?...? 無(wú)法異步工作

需要常規(guī)的同步 iterator 的功能,無(wú)法與異步 iterator 一起使用。

例如,spread 語(yǔ)法無(wú)法工作:

alert( [...range] ); // Error, no Symbol.iterator

這很正常,因?yàn)樗谕业?nbsp;Symbol.iterator,而不是 Symbol.asyncIterator

for..of 的情況和這個(gè)一樣:沒(méi)有 await 關(guān)鍵字時(shí),則期望找到的是 Symbol.iterator。

回顧 generator

現(xiàn)在,讓我們回顧一下 generator,它使我們能夠?qū)懗龈痰牡a。在大多數(shù)時(shí)候,當(dāng)我們想要?jiǎng)?chuàng)建一個(gè)可迭代對(duì)象時(shí),我們會(huì)使用 generator。

簡(jiǎn)單起見(jiàn),這里省略了一些解釋?zhuān)?generator 是“生成(yield)值的函數(shù)”。關(guān)于此的詳細(xì)說(shuō)明請(qǐng)見(jiàn) generator 一章。

Generator 是標(biāo)有 function*(注意星號(hào))的函數(shù),它使用 yield 來(lái)生成值,并且我們可以使用 for..of 循環(huán)來(lái)遍歷它們。

下面這例子生成了從 start 到 end 的一系列值:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

for(let value of generateSequence(1, 5)) {
  alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}

正如我們所知道的,要使一個(gè)對(duì)象可迭代,我們需要給它添加 Symbol.iterator。

let range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    return <帶有 next 方法的對(duì)象,以使對(duì)象 range 可迭代>
  }
}

對(duì)于 Symbol.iterator 來(lái)說(shuō),一個(gè)通常的做法是返回一個(gè) generator,這樣可以使代碼更短,如下所示:

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // [Symbol.iterator]: function*() 的一種簡(jiǎn)寫(xiě)
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

for(let value of range) {
  alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}

如果你想了解更多詳細(xì)內(nèi)容,請(qǐng)閱讀 generator 一章。

在常規(guī)的 generator 中,我們無(wú)法使用 await。所有的值都必須按照 for..of 構(gòu)造的要求同步地出現(xiàn)。

如果我們想要異步地生成值該怎么辦?例如,對(duì)于來(lái)自網(wǎng)絡(luò)請(qǐng)求的值。

讓我們?cè)倩氐疆惒?generator,來(lái)使這個(gè)需求成為可能。

異步 generator (finally)

對(duì)于大多數(shù)的實(shí)際應(yīng)用程序,當(dāng)我們想創(chuàng)建一個(gè)異步生成一系列值的對(duì)象時(shí),我們都可以使用異步 generator。

語(yǔ)法很簡(jiǎn)單:在 function* 前面加上 async。這即可使 generator 變?yōu)楫惒降摹?

然后使用 for await (...) 來(lái)遍歷它,像這樣:

async function* generateSequence(start, end) {

  for (let i = start; i <= end; i++) {

    // 哇,可以使用 await 了!
    await new Promise(resolve => setTimeout(resolve, 1000));

    yield i;
  }

}

(async () => {

  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    alert(value); // 1,然后 2,然后 3,然后 4,然后 5(在每個(gè) alert 之間有延遲)
  }

})();

因?yàn)榇?generator 是異步的,所以我們可以在其內(nèi)部使用 await,依賴(lài)于 promise,執(zhí)行網(wǎng)絡(luò)請(qǐng)求等任務(wù)。

引擎蓋下的差異

如果你還記得我們?cè)谇懊嬲鹿?jié)中所講的關(guān)于 generator 的細(xì)節(jié)知識(shí),那你應(yīng)該知道,從技術(shù)上講,異步 generator 和常規(guī)的 generator 在內(nèi)部是有區(qū)別的。

對(duì)于異步 generator,generator.next() 方法是異步的,它返回 promise。

在一個(gè)常規(guī)的 generator 中,我們使用 result = generator.next() 來(lái)獲得值。但在一個(gè)異步 generator 中,我們應(yīng)該添加 await 關(guān)鍵字,像這樣:

result = await generator.next(); // result = {value: ..., done: true/false}

這就是為什么異步 generator 可以與 for await...of 一起工作。

異步的可迭代對(duì)象 range

常規(guī)的 generator 可用作 Symbol.iterator 以使迭代代碼更短。

與之類(lèi)似,異步 generator 可用作 Symbol.asyncIterator 來(lái)實(shí)現(xiàn)異步迭代。

例如,我們可以通過(guò)將同步的 Symbol.iterator 替換為異步的 Symbol.asyncIterator,來(lái)使對(duì)象 range 異步地生成值,每秒生成一個(gè):

let range = {
  from: 1,
  to: 5,

  // 這一行等價(jià)于 [Symbol.asyncIterator]: async function*() {
  async *[Symbol.asyncIterator]() {
    for(let value = this.from; value <= this.to; value++) {

      // 在 value 之間暫停一會(huì)兒,等待一些東西
      await new Promise(resolve => setTimeout(resolve, 1000));

      yield value;
    }
  }
};

(async () => {

  for await (let value of range) {
    alert(value); // 1,然后 2,然后 3,然后 4,然后 5
  }

})();

現(xiàn)在,value 之間的延遲為 1 秒。

請(qǐng)注意:

從技術(shù)上講,我們可以把 Symbol.iterator 和 Symbol.asyncIterator 都添加到對(duì)象中,因此它既可以是同步的(for..of)也可以是異步的(for await..of)可迭代對(duì)象。

但是實(shí)際上,這將是一件很奇怪的事情。

實(shí)際的例子:分頁(yè)的數(shù)據(jù)

到目前為止,我們已經(jīng)了解了一些基本示例,以加深理解。現(xiàn)在,我們來(lái)看一個(gè)實(shí)際的用例。

目前,有很多在線(xiàn)服務(wù)都是發(fā)送的分頁(yè)的數(shù)據(jù)(paginated data)。例如,當(dāng)我們需要一個(gè)用戶(hù)列表時(shí),一個(gè)請(qǐng)求只返回一個(gè)預(yù)設(shè)數(shù)量的用戶(hù)(例如 100 個(gè)用戶(hù))—— “一頁(yè)”,并提供了指向下一頁(yè)的 URL。

這種模式非常常見(jiàn)。不僅可用于獲取用戶(hù)列表,這種模式還可以用于任意東西。

例如,GitHub 允許使用相同的分頁(yè)提交(paginated fashion)的方式找回 commit:

  • 我們應(yīng)該以 ?https://api.github.com/repos/<repo>/commits? 格式創(chuàng)建進(jìn)行 ?fetch? 的網(wǎng)絡(luò)請(qǐng)求。
  • 它返回一個(gè)包含 30 條 commit 的 JSON,并在返回的 ?Link? header 中提供了指向下一頁(yè)的鏈接。
  • 然后我們可以將該鏈接用于下一個(gè)請(qǐng)求,以獲取更多 commit,以此類(lèi)推。

對(duì)于我們的代碼,我們希望有一種更簡(jiǎn)單的獲取 commit 的方式。

讓我們創(chuàng)建一個(gè)函數(shù) fetchCommits(repo),用來(lái)在任何我們有需要的時(shí)候發(fā)出請(qǐng)求,來(lái)為我們獲取 commit。并且,該函數(shù)能夠關(guān)注到所有分頁(yè)內(nèi)容。對(duì)于我們來(lái)說(shuō),它將是一個(gè)簡(jiǎn)單的 for await..of 異步迭代。

因此,其用法將如下所示:

for await (let commit of fetchCommits("username/repository")) {
  // 處理 commit
}

通過(guò)異步 generator,我們可以輕松實(shí)現(xiàn)上面所描述的函數(shù),如下所示:

async function* fetchCommits(repo) {
  let url = `https://api.github.com/repos/${repo}/commits`;

  while (url) {
    const response = await fetch(url, { // (1)
      headers: {'User-Agent': 'Our script'}, // github 需要任意的 user-agent header
    });

    const body = await response.json(); // (2) 響應(yīng)的是 JSON(array of commits)

    // (3) 前往下一頁(yè)的 URL 在 header 中,提取它
    let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
    nextPage = nextPage?.[1];

    url = nextPage;

    for(let commit of body) { // (4) 一個(gè)接一個(gè)地 yield commit,直到最后一頁(yè)
      yield commit;
    }
  }
}

關(guān)于其工作原理的進(jìn)一步解釋?zhuān)?

  1. 我們使用瀏覽器的 fetch 方法來(lái)下載 commit。
    • 初始 URL 是 ?https://api.github.com/repos/<repo>/commits?,并且下一頁(yè)的 URL 將在響應(yīng)的 ?Link? header 中。
    • ?fetch? 方法允許我們提供授權(quán)和其他 header,如果需要 —— 這里 GitHub 需要的是 ?User-Agent?。
  2. commit 被以 JSON 的格式返回。
  3. 我們應(yīng)該從響應(yīng)(response)的 ?Link? header 中獲取前往下一頁(yè)的 URL。它有一個(gè)特殊的格式,所以我們對(duì)它使用正則表達(dá)式(我們將在 正則表達(dá)式 一章中學(xué)習(xí)它)。
    • 前往下一頁(yè)的 URL 看起來(lái)可能就像這樣 ?https://api.github.com/repositories/93253246/commits?page=2?。這是由 GitHub 自己生成的。
  4. 然后,我們將接收到的所有 commit 一個(gè)一個(gè)地 yield 出來(lái),當(dāng)所有 commit 都 yield 完成時(shí),將觸發(fā)下一個(gè) ?while(url)? 迭代,并發(fā)出下一個(gè)請(qǐng)求。

這是一個(gè)使用示例(在控制臺(tái)中顯示 commit 的作者)

(async () => {

  let count = 0;

  for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {

    console.log(commit.author.login);

    if (++count == 100) { // 讓我們?cè)讷@取了 100 個(gè) commit 時(shí)停止
      break;
    }
  }

})();

// 注意:如果你在外部沙箱中運(yùn)行它,你需要把上面的 fetchCommits 函數(shù)粘貼到這兒。

這就是我們想要的。

從外部看不到分頁(yè)請(qǐng)求(paginated requests)的內(nèi)部機(jī)制。對(duì)我們來(lái)說(shuō),它只是一個(gè)返回 commit 的異步 generator。

總結(jié)

常規(guī)的 iterator 和 generator 可以很好地處理那些不需要花費(fèi)時(shí)間來(lái)生成的的數(shù)據(jù)。

當(dāng)我們期望異步地,有延遲地獲取數(shù)據(jù)時(shí),可以使用它們的異步版本,并且使用 for await..of 替代 for..of

異步 iterator 與常規(guī) iterator 在語(yǔ)法上的區(qū)別:

Iterable 異步 Iterable
提供 iterator 的對(duì)象方法 Symbol.iterator Symbol.asyncIterator
next() 返回的值是 {value:…, done: true/false} resolve 成 {value:…, done: true/false} 的 Promise

異步 generator 與常規(guī) generator 在語(yǔ)法上的區(qū)別:

Generator 異步 generator
聲明方式 function* async function*
next() 返回的值是 {value:…, done: true/false} resolve 成 {value:…, done: true/false} 的 Promise

在 Web 開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到數(shù)據(jù)流,它們分段流動(dòng)(flows chunk-by-chunk)。例如,下載或上傳大文件。

我們可以使用異步 generator 來(lái)處理此類(lèi)數(shù)據(jù)。值得注意的是,在一些環(huán)境,例如瀏覽器環(huán)境下,還有另一個(gè)被稱(chēng)為 Streams 的 API,它提供了特殊的接口來(lái)處理此類(lèi)數(shù)據(jù)流,轉(zhuǎn)換數(shù)據(jù)并將數(shù)據(jù)從一個(gè)數(shù)據(jù)流傳遞到另一個(gè)數(shù)據(jù)流(例如,從一個(gè)地方下載并立即發(fā)送到其他地方)。


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)