新聞中心
1. 同步遍歷器的問題
《遍歷器》一章說過,Iterator 接口是一種數(shù)據(jù)遍歷的協(xié)議,只要調(diào)用遍歷器對象的next 方法,就會得到一個對象,表示當(dāng)前遍歷指針?biāo)诘哪莻€位置的信息。 next 方法返回的對象的結(jié)構(gòu)是{value, done} ,其中 value表示當(dāng)前的數(shù)據(jù)的值,done 是一個布爾值,表示遍歷是否結(jié)束。

10年積累的成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認識你,你也不認識我。但先網(wǎng)站設(shè)計后付款的網(wǎng)站建設(shè)流程,更有伊通免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
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() 方法,就返回一個對象,表示當(dāng)前遍歷位置的信息。
這里隱含著一個規(guī)定, it.next() 方法必須是同步的,只要調(diào)用就必須立刻返回值。也就是說,一旦執(zhí)行 it.next() 方法,就必須同步地得到 value 和 done 這兩個屬性。如果遍歷指針正好指向同步操作,當(dāng)然沒有問題,但對于異步操作,就不太合適了。
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)生。
2. 異步遍歷的接口
異步遍歷器的最大的語法特點,就是調(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 了,再返回一個表示當(dāng)前數(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();
3. 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 ###');
}
4. 異步 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)鍵字標(biāo)明,即 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ù)拋出錯誤,會導(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í)行,每當(dāng) 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)鍵字。
5. 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 當(dāng)前名稱:創(chuàng)新互聯(lián)ES6教程:ES6的異步遍歷器
地址分享:http://www.dlmjj.cn/article/dhdpgpd.html


咨詢
建站咨詢
