# 異步遍歷器
## 同步遍歷器的問題
《遍歷器》一章說過,Iterator 接口是一種數據遍歷的協議,只要調用遍歷器對象的`next`方法,就會得到一個對象,表示當前遍歷指針所在的那個位置的信息。`next`方法返回的對象的結構是`{value, done}`,其中`value`表示當前的數據的值,`done`是一個布爾值,表示遍歷是否結束。
```javascript
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)。每次調用`it.next()`方法,就返回一個對象,表示當前遍歷位置的信息。
這里隱含著一個規定,`it.next()`方法必須是同步的,只要調用就必須立刻返回值。也就是說,一旦執行`it.next()`方法,就必須同步地得到`value`和`done`這兩個屬性。如果遍歷指針正好指向同步操作,當然沒有問題,但對于異步操作,就不太合適了。
```javascript
function idMaker() {
let index = 0;
return {
next: function() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
resolve({ value: index++, done: false });
}, 1000);
});
}
};
}
```
上面代碼中,`next()`方法返回的是一個 Promise 對象,這樣就不行,不符合 Iterator 協議,只要代碼里面包含異步操作都不行。也就是說,Iterator 協議里面`next()`方法只能包含同步操作。
目前的解決方法是,將異步操作包裝成 Thunk 函數或者 Promise 對象,即`next()`方法返回值的`value`屬性是一個 Thunk 函數或者 Promise 對象,等待以后返回真正的值,而`done`屬性則還是同步產生的。
```javascript
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)) // 0
it.next().value.then(o => console.log(o)) // 1
it.next().value.then(o => console.log(o)) // 2
// ...
```
上面代碼中,`value`屬性的返回值是一個 Promise 對象,用來放置異步操作。但是這樣寫很麻煩,不太符合直覺,語義也比較繞。
ES2018 [引入](https://github.com/tc39/proposal-async-iteration)了“異步遍歷器”(Async Iterator),為異步操作提供原生的遍歷器接口,即`value`和`done`這兩個屬性都是異步產生。
## 異步遍歷的接口
異步遍歷器的最大的語法特點,就是調用遍歷器的`next`方法,返回的是一個 Promise 對象。
```javascript
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
```
上面代碼中,`asyncIterator`是一個異步遍歷器,調用`next`方法以后,返回一個 Promise 對象。因此,可以使用`then`方法指定,這個 Promise 對象的狀態變為`resolve`以后的回調函數。回調函數的參數,則是一個具有`value`和`done`兩個屬性的對象,這個跟同步遍歷器是一樣的。
我們知道,一個對象的同步遍歷器的接口,部署在`Symbol.iterator`屬性上面。同樣地,對象的異步遍歷器接口,部署在`Symbol.asyncIterator`屬性上面。不管是什么樣的對象,只要它的`Symbol.asyncIterator`屬性有值,就表示應該對它進行異步遍歷。
下面是一個異步遍歷器的例子。
```javascript
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 }
});
```
上面代碼中,異步遍歷器其實返回了兩次值。第一次調用的時候,返回一個 Promise 對象;等到 Promise 對象`resolve`了,再返回一個表示當前數據成員信息的對象。這就是說,異步遍歷器與同步遍歷器最終行為是一致的,只是會先返回 Promise 對象,作為中介。
由于異步遍歷器的`next`方法,返回的是一個 Promise 對象。因此,可以把它放在`await`命令后面。
```javascript
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`方法了。整個流程已經很接近同步處理了。
注意,異步遍歷器的`next`方法是可以連續調用的,不必等到上一步產生的 Promise 對象`resolve`以后再調用。這種情況下,`next`方法會累積起來,自動按照每一步的順序運行下去。下面是一個例子,把所有的`next`方法放在`Promise.all`方法里面。
```javascript
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
```
另一種用法是一次性調用所有的`next`方法,然后`await`最后一步操作。
```javascript
async function runner() {
const writer = openFile('someFile.txt');
writer.next('hello');
writer.next('world');
await writer.return();
}
runner();
```
## for await...of
前面介紹過,`for...of`循環用于遍歷同步的 Iterator 接口。新引入的`for await...of`循環,則是用于遍歷異步的 Iterator 接口。
```javascript
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
```
上面代碼中,`createAsyncIterable()`返回一個擁有異步遍歷器接口的對象,`for...of`循環自動調用這個對象的異步遍歷器的`next`方法,會得到一個 Promise 對象。`await`用來處理這個 Promise 對象,一旦`resolve`,就把得到的值(`x`)傳入`for...of`的循環體。
`for await...of`循環的一個用途,是部署了 asyncIterable 操作的異步接口,可以直接放入這個循環。
```javascript
let body = '';
async function f() {
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);
}
```
上面代碼中,`req`是一個 asyncIterable 對象,用來異步讀取數據。可以看到,使用`for await...of`循環以后,代碼會非常簡潔。
如果`next`方法返回的 Promise 對象被`reject`,`for await...of`就會報錯,要用`try...catch`捕捉。
```javascript
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
```
注意,`for await...of`循環也可以用于同步遍歷器。
```javascript
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b
```
Node v10 支持異步遍歷器,Stream 就部署了這個接口。下面是讀取文件的傳統寫法與異步遍歷器寫法的差異。
```javascript
// 傳統寫法
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 ###');
}
```
## 異步 Generator 函數
就像 Generator 函數返回一個同步遍歷器對象一樣,異步 Generator 函數的作用,是返回一個異步遍歷器對象。
在語法上,異步 Generator 函數就是`async`函數與 Generator 函數的結合。
```javascript
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
```
上面代碼中,`gen`是一個異步 Generator 函數,執行后返回一個異步 Iterator 對象。對該對象調用`next`方法,返回一個 Promise 對象。
異步遍歷器的設計目的之一,就是 Generator 函數處理同步操作和異步操作時,能夠使用同一套接口。
```javascript
// 同步 Generator 函數
function* map(iterable, func) {
const iter = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iter.next();
if (done) break;
yield func(value);
}
}
// 異步 Generator 函數
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 函數,第一個參數是可遍歷對象`iterable`,第二個參數是一個回調函數`func`。`map`的作用是將`iterable`每一步返回的值,使用`func`進行處理。上面有兩個版本的`map`,前一個處理同步遍歷器,后一個處理異步遍歷器,可以看到兩個版本的寫法基本上是一致的。
下面是另一個異步 Generator 函數的例子。
```javascript
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
```
上面代碼中,異步操作前面使用`await`關鍵字標明,即`await`后面的操作,應該返回 Promise 對象。凡是使用`yield`關鍵字的地方,就是`next`方法停下來的地方,它后面的表達式的值(即`await file.readLine()`的值),會作為`next()`返回對象的`value`屬性,這一點是與同步 Generator 函數一致的。
異步 Generator 函數內部,能夠同時使用`await`和`yield`命令。可以這樣理解,`await`命令用于將外部操作產生的值輸入函數內部,`yield`命令用于將函數內部的值輸出。
上面代碼定義的異步 Generator 函數的用法如下。
```javascript
(async function () {
for await (const line of readLines(filePath)) {
console.log(line);
}
})()
```
異步 Generator 函數可以與`for await...of`循環結合起來使用。
```javascript
async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
```
異步 Generator 函數的返回值是一個異步 Iterator,即每次調用它的`next`方法,會返回一個 Promise 對象,也就是說,跟在`yield`命令后面的,應該是一個 Promise 對象。如果像上面那個例子那樣,`yield`命令后面是一個字符串,會被自動包裝成一個 Promise 對象。
```javascript
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);
})
```
上面代碼中,`ag`是`asyncGenerator`函數返回的異步遍歷器對象。調用`ag.next()`以后,上面代碼的執行順序如下。
1. `ag.next()`立刻返回一個 Promise 對象。
1. `asyncGenerator`函數開始執行,打印出`Start`。
1. `await`命令返回一個 Promise 對象,`asyncGenerator`函數停在這里。
1. A 處變成 fulfilled 狀態,產生的值放入`result`變量,`asyncGenerator`函數繼續往下執行。
1. 函數在 B 處的`yield`暫停執行,一旦`yield`命令取到值,`ag.next()`返回的那個 Promise 對象變成 fulfilled 狀態。
1. `ag.next()`后面的`then`方法指定的回調函數開始執行。該回調函數的參數是一個對象`{value, done}`,其中`value`的值是`yield`命令后面的那個表達式的值,`done`的值是`false`。
A 和 B 兩行的作用類似于下面的代碼。
```javascript
return new Promise((resolve, reject) => {
fetchRandom()
.then(result => result.text())
.then(result => {
resolve({
value: 'Result: ' + result,
done: false,
});
});
});
```
如果異步 Generator 函數拋出錯誤,會導致 Promise 對象的狀態變為`reject`,然后拋出的錯誤被`catch`方法捕獲。
```javascript
async function* asyncGenerator() {
throw new Error('Problem!');
}
asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!
```
注意,普通的 async 函數返回的是一個 Promise 對象,而異步 Generator 函數返回的是一個異步 Iterator 對象。可以這樣理解,async 函數和異步 Generator 函數,是封裝異步操作的兩種方法,都用來達到同一種目的。區別在于,前者自帶執行器,后者通過`for await...of`執行,或者自己編寫執行器。下面就是一個異步 Generator 函數的執行器。
```javascript
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 函數產生的異步遍歷器,會通過`while`循環自動執行,每當`await iterator.next()`完成,就會進入下一輪循環。一旦`done`屬性變為`true`,就會跳出循環,異步遍歷器執行結束。
下面是這個自動執行器的一個使用實例。
```javascript
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 函數出現以后,JavaScript 就有了四種函數形式:普通函數、async 函數、Generator 函數和異步 Generator 函數。請注意區分每種函數的不同之處。基本上,如果是一系列按照順序執行的異步操作(比如讀取文件,然后寫入新內容,再存入硬盤),可以使用 async 函數;如果是一系列產生相同數據結構的異步操作(比如一行一行讀取文件),可以使用異步 Generator 函數。
異步 Generator 函數也可以通過`next`方法的參數,接收外部傳入的數據。
```javascript
const writer = openFile('someFile.txt');
writer.next('hello'); // 立即執行
writer.next('world'); // 立即執行
await writer.return(); // 等待寫入結束
```
上面代碼中,`openFile`是一個異步 Generator 函數。`next`方法的參數,向該函數內部的操作傳入數據。每次`next`方法都是同步執行的,最后的`await`命令用于等待整個寫入操作結束。
最后,同步的數據結構,也可以使用異步 Generator 函數。
```javascript
async function* createAsyncIterable(syncIterable) {
for (const elem of syncIterable) {
yield elem;
}
}
```
上面代碼中,由于沒有異步操作,所以也就沒有使用`await`關鍵字。
## yield\* 語句
`yield*`語句也可以跟一個異步遍歷器。
```javascript
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最終會等于 2
const result = yield* gen1();
}
```
上面代碼中,`gen2`函數里面的`result`變量,最后的值是`2`。
與同步 Generator 函數一樣,`for await...of`循環會展開`yield*`。
```javascript
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// a
// b
```
- 前言
- ECMAScript 6簡介
- let 和 const 命令
- 變量的解構賦值
- 字符串的擴展
- 字符串的新增方法
- 正則的擴展
- 數值的擴展
- 函數的擴展
- 數組的擴展
- 對象的擴展
- 對象的新增方法
- Symbol
- Set 和 Map 數據結構
- Proxy
- Reflect
- Promise 對象
- Iterator 和 for...of 循環
- Generator 函數的語法
- Generator 函數的異步應用
- async 函數
- Class 的基本語法
- Class 的繼承
- Module 的語法
- Module 的加載實現
- 編程風格
- 讀懂規格
- 異步遍歷器
- ArrayBuffer
- 最新提案
- Decorator
- 參考鏈接