僅為方便個人查詢使用
來源:http://es6.ruanyifeng.com/#docs/async-iterator
同步遍歷器的問題
《遍歷器》一章說過域那,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 await...of
前面介紹過异吻,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ù)的作用,是返回一個異步遍歷器對象括改。
在語法上腻豌,異步 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ù)停在這里。 - A 處變成 fulfilled 狀態(tài)佳励,產(chǎn)生的值放入
result
變量休里,asyncGenerator
函數(shù)繼續(xù)往下執(zhí)行。 - 函數(shù)在 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ù)拋出錯誤巴元,會導致 Promise 對象的狀態(tài)變?yōu)?code>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)?code>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* 語句
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