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