W3Cschool
恭喜您成為首批注冊用戶
獲得88經驗值獎勵
異步迭代允許我們對按需通過異步請求而得到的數據進行迭代。例如,我們通過網絡分段(chunk-by-chunk)下載數據時。異步生成器(generator)使這一步驟更加方便。
首先,讓我們來看一個簡單的示例以掌握語法,然后再看一個實際用例。
讓我們回顧一下可迭代對象的相關內容。
假設我們有一個對象,例如下面的 range
:
let range = {
from: 1,
to: 5
};
我們想對它使用 for..of
循環(huán),例如 for(value of range)
,來獲取從 1
到 5
的值。
換句話說,我們想向對象 range
添加 迭代能力。
這可以通過使用一個名為 Symbol.iterator
的特殊方法來實現:
for..of
? 結構調用,并且它應該返回一個帶有 ?next
? 方法的對象。next()
? 方法。next()
? 方法應該以 ?{done: true/false, value:<loop value>}
? 的格式返回一個值,其中 ?done:true
? 表示循環(huán)結束。這是可迭代的 range
的一個實現:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() { // 在 for..of 循環(huán)開始時被調用一次
return {
current: this.from,
last: this.to,
next() { // 每次迭代時都會被調用,來獲取下一個值
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(可迭代對象) 一章,其中詳細講解了關于常規(guī)迭代器(iterator)的所有內容。
當值是以異步的形式出現時,例如在 setTimeout
或者另一種延遲之后,就需要異步迭代。
最常見的場景是,對象需要發(fā)送一個網絡請求以傳遞下一個值,稍后我們將看到一個它的真實示例。
要使對象異步迭代:
Symbol.asyncIterator
? 取代 ?Symbol.iterator
?。next()
? 方法應該返回一個 ?promise
?(帶有下一個值,并且狀態(tài)為 ?fulfilled
?)。async
? 可以實現這一點,我們可以簡單地使用 ?async next()
?。for await (let item of iterable)
? 循環(huán)來迭代這樣的對象。await
?。作為開始的示例,讓我們創(chuàng)建一個可迭代的 range
對象,與前面的那個類似,不過現在它將異步地每秒返回一個值。
我們需要做的就是對上面代碼中的部分代碼進行替換:
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() { // (1)
return {
current: this.from,
last: this.to,
async next() { // (2)
// 注意:我們可以在 async next 內部使用 "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
}
})()
正如我們所看到的,其結構與常規(guī)的 iterator 類似:
Symbol.asyncIterator
?(1)?
?。next()
? 方法的對象,?next()
? 方法會返回一個 promise ?(2)
?。next()
? 方法可以不是 ?async
? 的,它可以是一個返回值是一個 ?promise
? 的常規(guī)的方法,但是使用 ?async
? 關鍵字可以允許我們在方法內部使用 ?await
?,所以會更加方便。這里我們只是用于延遲 1 秒的操作 ?(3)
?。for await(let value of range)
? ?(4)
? 來進行迭代,也就是在 ?for
? 后面添加 ?await
?。它會調用一次 ?range[Symbol.asyncIterator]()
? 方法一次,然后調用它的 ?next()
? 方法獲取值。這是一個對比 Iterator 和異步 iterator 之間差異的表格:
Iterator | 異步 iterator | |
---|---|---|
提供 iterator 的對象方法 | Symbol.iterator
|
Symbol.asyncIterator
|
next() 返回的值是 |
任意值 | Promise
|
要進行循環(huán),使用 | for..of
|
for await..of
|
Spread 語法 ?
...
? 無法異步工作需要常規(guī)的同步 iterator 的功能,無法與異步 iterator 一起使用。
例如,spread 語法無法工作:
alert( [...range] ); // Error, no Symbol.iterator
這很正常,因為它期望找到
Symbol.iterator
,而不是Symbol.asyncIterator
。
for..of
的情況和這個一樣:沒有await
關鍵字時,則期望找到的是Symbol.iterator
。
現在,讓我們回顧一下 generator,它使我們能夠寫出更短的迭代代碼。在大多數時候,當我們想要創(chuàng)建一個可迭代對象時,我們會使用 generator。
簡單起見,這里省略了一些解釋,即 generator 是“生成(yield)值的函數”。關于此的詳細說明請見 generator 一章。
Generator 是標有 function*
(注意星號)的函數,它使用 yield
來生成值,并且我們可以使用 for..of
循環(huán)來遍歷它們。
下面這例子生成了從 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
}
正如我們所知道的,要使一個對象可迭代,我們需要給它添加 Symbol.iterator
。
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return <帶有 next 方法的對象,以使對象 range 可迭代>
}
}
對于 Symbol.iterator
來說,一個通常的做法是返回一個 generator,這樣可以使代碼更短,如下所示:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*() 的一種簡寫
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
for(let value of range) {
alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}
如果你想了解更多詳細內容,請閱讀 generator 一章。
在常規(guī)的 generator 中,我們無法使用 await
。所有的值都必須按照 for..of
構造的要求同步地出現。
如果我們想要異步地生成值該怎么辦?例如,對于來自網絡請求的值。
讓我們再回到異步 generator,來使這個需求成為可能。
對于大多數的實際應用程序,當我們想創(chuàng)建一個異步生成一系列值的對象時,我們都可以使用異步 generator。
語法很簡單:在 function*
前面加上 async
。這即可使 generator 變?yōu)楫惒降摹?
然后使用 for await (...)
來遍歷它,像這樣:
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(在每個 alert 之間有延遲)
}
})();
因為此 generator 是異步的,所以我們可以在其內部使用 await
,依賴于 promise
,執(zhí)行網絡請求等任務。
引擎蓋下的差異
如果你還記得我們在前面章節(jié)中所講的關于 generator 的細節(jié)知識,那你應該知道,從技術上講,異步 generator 和常規(guī)的 generator 在內部是有區(qū)別的。
對于異步 generator,
generator.next()
方法是異步的,它返回 promise。
在一個常規(guī)的 generator 中,我們使用
result = generator.next()
來獲得值。但在一個異步 generator 中,我們應該添加await
關鍵字,像這樣:
result = await generator.next(); // result = {value: ..., done: true/false}
這就是為什么異步 generator 可以與
for await...of
一起工作。
常規(guī)的 generator 可用作 Symbol.iterator
以使迭代代碼更短。
與之類似,異步 generator 可用作 Symbol.asyncIterator
來實現異步迭代。
例如,我們可以通過將同步的 Symbol.iterator
替換為異步的 Symbol.asyncIterator
,來使對象 range
異步地生成值,每秒生成一個:
let range = {
from: 1,
to: 5,
// 這一行等價于 [Symbol.asyncIterator]: async function*() {
async *[Symbol.asyncIterator]() {
for(let value = this.from; value <= this.to; value++) {
// 在 value 之間暫停一會兒,等待一些東西
await new Promise(resolve => setTimeout(resolve, 1000));
yield value;
}
}
};
(async () => {
for await (let value of range) {
alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}
})();
現在,value 之間的延遲為 1 秒。
請注意:
從技術上講,我們可以把
Symbol.iterator
和Symbol.asyncIterator
都添加到對象中,因此它既可以是同步的(for..of
)也可以是異步的(for await..of
)可迭代對象。
但是實際上,這將是一件很奇怪的事情。
到目前為止,我們已經了解了一些基本示例,以加深理解?,F在,我們來看一個實際的用例。
目前,有很多在線服務都是發(fā)送的分頁的數據(paginated data)。例如,當我們需要一個用戶列表時,一個請求只返回一個預設數量的用戶(例如 100 個用戶)—— “一頁”,并提供了指向下一頁的 URL。
這種模式非常常見。不僅可用于獲取用戶列表,這種模式還可以用于任意東西。
例如,GitHub 允許使用相同的分頁提交(paginated fashion)的方式找回 commit:
https://api.github.com/repos/<repo>/commits
? 格式創(chuàng)建進行 ?fetch
? 的網絡請求。Link
? header 中提供了指向下一頁的鏈接。對于我們的代碼,我們希望有一種更簡單的獲取 commit 的方式。
讓我們創(chuàng)建一個函數 fetchCommits(repo)
,用來在任何我們有需要的時候發(fā)出請求,來為我們獲取 commit。并且,該函數能夠關注到所有分頁內容。對于我們來說,它將是一個簡單的 for await..of
異步迭代。
因此,其用法將如下所示:
for await (let commit of fetchCommits("username/repository")) {
// 處理 commit
}
通過異步 generator,我們可以輕松實現上面所描述的函數,如下所示:
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) 響應的是 JSON(array of commits)
// (3) 前往下一頁的 URL 在 header 中,提取它
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
nextPage = nextPage?.[1];
url = nextPage;
for(let commit of body) { // (4) 一個接一個地 yield commit,直到最后一頁
yield commit;
}
}
}
關于其工作原理的進一步解釋:
https://api.github.com/repos/<repo>/commits
?,并且下一頁的 URL 將在響應的 ?Link
? header 中。fetch
? 方法允許我們提供授權和其他 header,如果需要 —— 這里 GitHub 需要的是 ?User-Agent
?。Link
? header 中獲取前往下一頁的 URL。它有一個特殊的格式,所以我們對它使用正則表達式(我們將在 正則表達式 一章中學習它)。https://api.github.com/repositories/93253246/commits?page=2
?。這是由 GitHub 自己生成的。while(url)
? 迭代,并發(fā)出下一個請求。這是一個使用示例(在控制臺中顯示 commit 的作者)
(async () => {
let count = 0;
for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
console.log(commit.author.login);
if (++count == 100) { // 讓我們在獲取了 100 個 commit 時停止
break;
}
}
})();
// 注意:如果你在外部沙箱中運行它,你需要把上面的 fetchCommits 函數粘貼到這兒。
這就是我們想要的。
從外部看不到分頁請求(paginated requests)的內部機制。對我們來說,它只是一個返回 commit 的異步 generator。
常規(guī)的 iterator 和 generator 可以很好地處理那些不需要花費時間來生成的的數據。
當我們期望異步地,有延遲地獲取數據時,可以使用它們的異步版本,并且使用 for await..of
替代 for..of
。
異步 iterator 與常規(guī) iterator 在語法上的區(qū)別:
Iterable | 異步 Iterable | |
---|---|---|
提供 iterator 的對象方法 | Symbol.iterator
|
Symbol.asyncIterator
|
next() 返回的值是 |
{value:…, done: true/false}
|
resolve 成 {value:…, done: true/false} 的 Promise
|
異步 generator 與常規(guī) generator 在語法上的區(qū)別:
Generator | 異步 generator | |
---|---|---|
聲明方式 | function*
|
async function*
|
next() 返回的值是 |
{value:…, done: true/false}
|
resolve 成 {value:…, done: true/false} 的 Promise
|
在 Web 開發(fā)中,我們經常會遇到數據流,它們分段流動(flows chunk-by-chunk)。例如,下載或上傳大文件。
我們可以使用異步 generator 來處理此類數據。值得注意的是,在一些環(huán)境,例如瀏覽器環(huán)境下,還有另一個被稱為 Streams 的 API,它提供了特殊的接口來處理此類數據流,轉換數據并將數據從一個數據流傳遞到另一個數據流(例如,從一個地方下載并立即發(fā)送到其他地方)。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯系方式:
更多建議: