非同步迭代讓我們得以依需求非同步地反覆處理資料。例如,當我們透過網路分塊下載某項內容時。非同步產生器讓這件事變得更方便。
讓我們先看一個簡單的範例來了解語法,然後再檢視實際的用例。
回顧可迭代物件
讓我們回顧一下可迭代物件的主題。
概念是我們有一個物件,例如這裡的 range
let range = {
from: 1,
to: 5
};
…我們希望對它使用 for..of
迴圈,例如 for(value of range)
,來取得 1
到 5
的值。
換句話說,我們希望為物件新增迭代能力。
這可以使用一個名稱為 Symbol.iterator
的特殊方法來實作
- 當迴圈開始時,
for..of
建構會呼叫此方法,它應該傳回一個具有next
方法的物件。 - 對於每個迭代,都會呼叫
next()
方法以取得下一個值。 next()
應該傳回{done: true/false, value:<loop value>}
形式的值,其中done:true
表示迴圈結束。
以下是可迭代 range
的實作
let range = {
from: 1,
to: 5,
[Symbol.iterator]() { // called once, in the beginning of for..of
return {
current: this.from,
last: this.to,
next() { // called every iteration, to get the next value
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
for(let value of range) {
alert(value); // 1 then 2, then 3, then 4, then 5
}
如果還有不清楚的地方,請參閱章節 可迭代,它提供了有關一般可迭代的所有詳細資料。
非同步可迭代
當值以非同步方式出現時,需要非同步迭代:在 setTimeout
或其他類型的延遲之後。
最常見的情況是,物件需要發出網路要求才能傳遞下一個值,我們稍後會看到一個實際範例。
要讓物件以非同步方式可迭代
- 使用
Symbol.asyncIterator
取代Symbol.iterator
。 next()
方法應傳回一個承諾(以下一個值來完成)。async
關鍵字會處理它,我們可以簡單地建立async next()
。
- 要迭代這樣的物件,我們應使用
for await (let item of iterable)
迴圈。- 請注意
await
字詞。
- 請注意
作為一個開始的範例,讓我們建立一個可迭代 range
物件,類似於前一個,但現在它會以非同步方式傳回值,每秒一個。
我們只需要在上述程式碼中執行一些替換
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() { // (1)
return {
current: this.from,
last: this.to,
async next() { // (2)
// note: we can use "await" inside the async next:
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
}
})()
正如我們所見,結構類似於一般迭代器
- 要讓一個物件以非同步方式可迭代,它必須有一個方法
Symbol.asyncIterator
(1)
。 - 這個方法必須傳回一個物件,其
next()
方法傳回一個承諾(2)
。 next()
方法不必是async
,它可以是一個傳回承諾的一般方法,但async
允許我們使用await
,因此很方便。這裡我們只延遲一秒(3)
。- 要迭代,我們使用
for await(let value of range)
(4)
,即在「for」之後加上「await」。它會呼叫range[Symbol.asyncIterator]()
一次,然後呼叫其next()
來取得值。
以下是一個顯示差異的小表格
迭代器 | 非同步迭代器 | |
---|---|---|
提供迭代器的物件方法 | Symbol.iterator |
Symbol.asyncIterator |
next() 傳回值是 |
任何值 | 承諾 |
要迴圈,請使用 | for..of |
for await..of |
...
非同步運作需要一般同步迭代器的功能,無法與非同步迭代器搭配使用。
例如,散佈語法無法運作
alert( [...range] ); // Error, no Symbol.iterator
這是正常的,因為它預期找到 Symbol.iterator
,而不是 Symbol.asyncIterator
。
for..of
也是如此:沒有 await
的語法需要 Symbol.iterator
。
回顧產生器
現在讓我們回顧一下產生器,因為它們可以讓迭代程式碼更簡短。大部分時間,當我們想要建立一個可迭代物件時,我們會使用產生器。
為了簡潔起見,省略了一些重要的東西,它們是「產生(讓出)值的函式」。它們在章節 產生器 中有詳細說明。
產生器標記為 function*
(注意星號),並使用 yield
產生一個值,然後我們可以使用 for..of
迴圈它們。
此範例產生一個從 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, then 2, then 3, then 4, then 5
}
正如我們所知,要建立一個可迭代物件,我們應該新增 Symbol.iterator
到其中。
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return <object with next to make range iterable>
}
}
Symbol.iterator
的常見做法是回傳一個產生器,它可以縮短程式碼,如您所見
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
for(let value of range) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
如果您想要更多詳細資訊,請參閱章節 產生器。
在一般產生器中,我們無法使用 await
。所有值都必須同步傳遞,這是 for..of
結構所要求的。
如果我們想要非同步產生值怎麼辦?例如,從網路要求中。
讓我們切換到非同步產生器以實現這個功能。
非同步產生器(最後)
對於大多數實際應用,當我們想要建立一個非同步產生一系列值的物件時,我們可以使用非同步產生器。
語法很簡單:在 function*
前加上 async
。這會讓產生器變成非同步。
然後使用 for await (...)
迭代它,如下所示
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
// Wow, can use 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, then 2, then 3, then 4, then 5 (with delay between)
}
})();
由於產生器是異步的,我們可以在其中使用await
,依賴承諾,執行網路請求等等。
技術上來說,如果您是一位進階讀者,記得有關產生器的詳細資訊,那麼內部會有些差異。
對於非同步產生器,generator.next()
方法是非同步的,它會傳回承諾。
在一般產生器中,我們會使用result = generator.next()
來取得值。在非同步產生器中,我們應該加入await
,如下所示
result = await generator.next(); // result = {value: ..., done: true/false}
這就是為什麼非同步產生器會使用for await...of
。
非同步可迭代範圍
一般產生器可以用作Symbol.iterator
,以縮短迭代程式碼。
類似地,非同步產生器可以用作Symbol.asyncIterator
來實作非同步迭代。
例如,我們可以讓range
物件非同步產生值,每秒一次,方法是將同步的Symbol.iterator
替換為非同步的Symbol.asyncIterator
let range = {
from: 1,
to: 5,
// this line is same as [Symbol.asyncIterator]: async function*() {
async *[Symbol.asyncIterator]() {
for(let value = this.from; value <= this.to; value++) {
// make a pause between values, wait for something
await new Promise(resolve => setTimeout(resolve, 1000));
yield value;
}
}
};
(async () => {
for await (let value of range) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
})();
現在值之間會延遲 1 秒。
技術上來說,我們可以將Symbol.iterator
和Symbol.asyncIterator
都新增到物件,因此它既可以同步(for..of
)又可以非同步(for await..of
)迭代。
不過,在實務上,那會是一件很奇怪的事。
實際範例:分頁資料
到目前為止,我們已經看過一些基本範例,以加深了解。現在讓我們檢視一個實際的用例。
有許多線上服務會提供分頁資料。例如,當我們需要使用者清單時,要求會傳回預先定義的數量(例如 100 位使用者)–「一頁」,並提供下一個頁面的網址。
這種模式非常常見。它不只適用於使用者,而是適用於任何事物。
例如,GitHub 允許我們以相同的、分頁的方式擷取提交記錄
- 我們應該以
https://api.github.com/repos/<repo>/commits
格式向fetch
提出要求。 - 它會以 30 筆提交記錄的 JSON 檔回應,並在
Link
標頭中提供下一個頁面的連結。 - 然後,我們可以使用該連結進行下一個要求,以取得更多提交記錄,依此類推。
對於我們的程式碼,我們希望有一個更簡單的方法來取得提交記錄。
讓我們建立一個函式fetchCommits(repo)
,它會為我們取得提交記錄,並在需要時提出要求。讓它處理所有分頁相關的事項。對我們來說,這只會是一個簡單的非同步迭代for await..of
。
因此,用法如下
for await (let commit of fetchCommits("username/repository")) {
// process commit
}
以下是一個以非同步產生器實作的函式
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 needs any user-agent header
});
const body = await response.json(); // (2) response is JSON (array of commits)
// (3) the URL of the next page is in the headers, extract it
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
nextPage = nextPage?.[1];
url = nextPage;
for(let commit of body) { // (4) yield commits one by one, until the page ends
yield commit;
}
}
}
更多關於其運作方式的說明
-
我們使用瀏覽器的 fetch 方法來下載提交。
- 初始 URL 為
https://api.github.com/repos/<repo>/commits
,而下一頁將會在回應的Link
標頭中。 fetch
方法允許我們提供授權和其他標頭(如果需要的話)——在此處 GitHub 需要User-Agent
。
- 初始 URL 為
-
提交以 JSON 格式傳回。
-
我們應該從回應的
Link
標頭取得下一頁的 URL。它有特殊格式,因此我們使用正規表示法(我們將在 正規表示法 中學習此功能)。- 下一頁的 URL 可能看起來像
https://api.github.com/repositories/93253246/commits?page=2
。它是由 GitHub 本身產生的。
- 下一頁的 URL 可能看起來像
-
然後我們逐一產生接收到的提交,而當它們完成時,下一個
while(url)
迭代將觸發,執行更多請求。
使用範例(在主控台中顯示提交作者)
(async () => {
let count = 0;
for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
console.log(commit.author.login);
if (++count == 100) { // let's stop at 100 commits
break;
}
}
})();
// Note: If you are running this in an external sandbox, you'll need to paste here the function fetchCommits described above
這正是我們想要的。
分頁請求的內部機制對外部來說是不可見的。對我們來說,它只是一個傳回提交的非同步產生器。
摘要
常規迭代器和產生器適用於不會花費時間產生的資料。
當我們預期資料會非同步地延遲到來時,可以使用它們的非同步對應項目,並使用 for await..of
取代 for..of
。
非同步迭代器和常規迭代器之間的語法差異
可迭代 | 非同步可迭代 | |
---|---|---|
提供迭代器的函數 | Symbol.iterator |
Symbol.asyncIterator |
next() 傳回值是 |
{value:…, done: true/false} |
Promise 解析為 {value:…, done: true/false} |
非同步產生器和常規產生器之間的語法差異
產生器 | 非同步產生器 | |
---|---|---|
宣告 | function* |
async function* |
next() 傳回值是 |
{value:…, done: true/false} |
Promise 解析為 {value:…, done: true/false} |
在網頁開發中,我們經常會遇到資料串流,當它一塊一塊地流動時。例如,下載或上傳一個大檔案。
我們可以使用非同步產生器來處理此類資料。值得注意的是,在某些環境中,例如在瀏覽器中,還有一個稱為串流的 API,它提供了特殊介面來處理此類串流,以轉換資料並將其從一個串流傳遞到另一個串流(例如從一個地方下載並立即傳送到其他地方)。
留言
<code>
標籤,對於多行程式碼 - 請用<pre>
標籤將其包起來,對於超過 10 行的程式碼 - 請使用沙盒 (plnkr、jsbin、codepen…)