ES9的新特性:非同步遍歷Async iteration
簡介
在ES6中,引入了同步iteration的概念,隨著ES8中的Async操作符的引用,是不是可以在一非同步操作中進行遍歷操作呢?
今天要給大家講一講ES9中的非同步遍歷的新特性Async iteration。
非同步遍歷
在講解非同步遍歷之前,我們先回想一下ES6中的同步遍歷。
根據ES6的定義,iteration主要由三部分組成:
- Iterable
先看下Iterable的定義:
interface Iterable {
[Symbol.iterator]() : Iterator;
}
Iterable表示這個物件裡面有可遍歷的資料,並且需要實現一個可以生成Iterator的工廠方法。
- Iterator
interface Iterator {
next() : IteratorResult;
}
可以從Iterable中構建Iterator。Iterator是一個類似遊標的概念,可以通過next訪問到IteratorResult。
- IteratorResult
IteratorResult是每次呼叫next方法得到的資料。
interface IteratorResult {
value: any;
done: boolean;
}
IteratorResult中除了有一個value值表示要獲取到的資料之外,還有一個done,表示是否遍歷完成。
下面是一個遍歷陣列的例子:
> const iterable = ['a', 'b'];
> const iterator = iterable[Symbol.iterator]();
> iterator.next()
{ value: 'a', done: false }
> iterator.next()
{ value: 'b', done: false }
> iterator.next()
{ value: undefined, done: true }
但是上的例子遍歷的是同步資料,如果我們獲取的是非同步資料,比如從http端下載下來的檔案,我們想要一行一行的對檔案進行遍歷。因為讀取一行資料是非同步操作,那麼這就涉及到了非同步資料的遍歷。
加入非同步讀取檔案的方法是readLinesFromFile,那麼同步的遍歷方法,對非同步來說就不再適用了:
//不再適用
for (const line of readLinesFromFile(fileName)) {
console.log(line);
}
也許你會想,我們是不是可以把非同步讀取一行的操作封裝在Promise中,然後用同步的方式去遍歷呢?
想法很好,不過這種情況下,非同步操作是否執行完畢是無法檢測到的。所以方法並不可行。
於是ES9引入了非同步遍歷的概念:
-
可以通過Symbol.asyncIterator來獲取到非同步iterables中的iterator。
-
非同步iterator的next()方法返回Promises物件,其中包含IteratorResults。
所以,我們看下非同步遍歷的API定義:
interface AsyncIterable {
[Symbol.asyncIterator]() : AsyncIterator;
}
interface AsyncIterator {
next() : Promise<IteratorResult>;
}
interface IteratorResult {
value: any;
done: boolean;
}
我們看一個非同步遍歷的應用:
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 }
});
其中createAsyncIterable將會把一個同步的iterable轉換成一個非同步的iterable,我們將會在下面一小節中看一下到底怎麼生成的。
這裡我們主要關注一下asyncIterator的遍歷操作。
因為ES8中引入了Async操作符,我們也可以把上面的程式碼,使用Async函式重寫:
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 }
}
非同步iterable的遍歷
使用for-of可以遍歷同步iterable,使用 for-await-of 可以遍歷非同步iterable。
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// Output:
// a
// b
注意,await需要放在async函式中才行。
如果我們的非同步遍歷中出現異常,則可以在 for-await-of 中使用try catch來捕獲這個異常:
function createRejectingIterable() {
return {
[Symbol.asyncIterator]() {
return this;
},
next() {
return Promise.reject(new Error('Problem!'));
},
};
}
(async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
// Error: Problem!
}
})();
同步的iterable返回的是同步的iterators,next方法返回的是{value, done}。
如果使用 for-await-of 則會將同步的iterators轉換成為非同步的iterators。然後返回的值被轉換成為了Promise。
如果同步的next本身返回的value就是Promise物件,則非同步的返回值還是同樣的promise。
也就是說會把:Iterable<Promise<T>>
轉換成為 AsyncIterable<T>
,如下面的例子所示:
async function main() {
const syncIterable = [
Promise.resolve('a'),
Promise.resolve('b'),
];
for await (const x of syncIterable) {
console.log(x);
}
}
main();
// Output:
// a
// b
上面的例子將同步的Promise轉換成非同步的Promise。
async function main() {
for await (const x of ['a', 'b']) {
console.log(x);
}
}
main();
// Output:
// c
// d
上面的例子將同步的常量轉換成為Promise。 可以看到兩者的結果是一樣的。
非同步iterable的生成
回到上面的例子,我們使用createAsyncIterable(syncIterable)將syncIterable轉換成了AsyncIterable。
我們看下這個方法是怎麼實現的:
async function* createAsyncIterable(syncIterable) {
for (const elem of syncIterable) {
yield elem;
}
}
上面的程式碼中,我們在一個普通的generator function前面加上async,表示的是非同步的generator。
對於普通的generator來說,每次呼叫next方法的時候,都會返回一個object {value,done} ,這個object物件是對yield值的封裝。
對於一個非同步的generator來說,每次呼叫next方法的時候,都會返回一個包含object {value,done} 的promise物件。這個object物件是對yield值的封裝。
因為返回的是Promise物件,所以我們不需要等待非同步執行的結果完成,就可以再次呼叫next方法。
我們可以通過一個Promise.all來同時執行所有的非同步Promise操作:
const asyncGenObj = createAsyncIterable(['a', 'b']);
const [{value:v1},{value:v2}] = await Promise.all([
asyncGenObj.next(), asyncGenObj.next()
]);
console.log(v1, v2); // a b
在createAsyncIterable中,我們是從同步的Iterable中建立非同步的Iterable。
接下來我們看下如何從非同步的Iterable中建立非同步的Iterable。
從上一節我們知道,可以使用for-await-of 來讀取非同步Iterable的資料,於是我們可以這樣用:
async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
在generator一文中,我們講到了在generator中呼叫generator。也就是在一個生產器中通過使用yield*來呼叫另外一個生成器。
同樣的,如果是在非同步生成器中,我們可以做同樣的事情:
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
const result = yield* gen1();
// result === 2
}
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// Output:
// a
// b
如果在非同步生成器中丟擲異常,這個異常也會被封裝在Promise中:
async function* asyncGenerator() {
throw new Error('Problem!');
}
asyncGenerator().next()
.catch(err => console.log(err)); // Error: Problem!
非同步方法和非同步生成器
非同步方法是使用async function 宣告的方法,它會返回一個Promise物件。
function中的return或throw異常會作為返回的Promise中的value。
(async function () {
return 'hello';
})()
.then(x => console.log(x)); // hello
(async function () {
throw new Error('Problem!');
})()
.catch(x => console.error(x)); // Error: Problem!
非同步生成器是使用 async function * 申明的方法。它會返回一個非同步的iterable。
通過呼叫iterable的next方法,將會返回一個Promise。非同步生成器中yield 的值會用來填充Promise的值。如果在生成器中丟擲了異常,同樣會被Promise捕獲到。
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
本文作者:flydean程式那些事
本文連結:http://www.flydean.com/es9-async-iteration/
本文來源:flydean的部落格
歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!