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