在JavaScript,ES6有Promise作為非同步處理的標準API,ES8加入async、await,便於以同步風格來撰寫非同步操作,隨著逐漸熟悉async、await,會遇上Symbol.iterator協定實作非同步迭代時的問題,為此ES9新增非同步迭代的語法。

ES6同步迭代協定、for-of

想認識ES9新增的非同步迭代相關語法,須從ES6定義的迭代協定開始認識,例如,對於字串、陣列或自訂群集等物件,開發者也許會想要迭代其內容,然而在ES6前並沒有一致的協定。

ES6規範可迭代物件須具有Symbol.iterator特性,它參考至函式,函式執行過後會傳回迭代器,迭代器必須具有next方法,每次呼叫會傳回一個迭代器結果(IteratorResult)物件,特性需包含value與done,value是當次迭代值,done為表示迭代是否結束的布林值,當迭代結束時,value必須是undefined。

開發者可以自行操作迭代器,在while中檢查迭代器結果物件的done值,以判斷是否結束迭代,不過,使用ES6的for-of來迭代會更為方便,例如,物件obj若具有Symbol.iterator協定,就可以用for(let v of obj) { console.log(v); }來迭代。

那麼,Symbol.iterator特性參考的函式如何定義?自行實作函式時,傳回一個具有next方法的物件,而next方法實作中,要判斷當次迭代是否有結果,並傳回一個具有value與done特性的物件……由於這類的實現過於麻煩,因而ES6提供了產生器語法,可以透過function*與yield來定義函式,作為Symbol.iterator的特性,被yield的值成為結果物件value特性,done會是false,產生器函式return的話,value會是undefined,done會是true,因此實作上方便直覺。

對於JavaScript的開發者來說,應是十分熟悉這些ES6基本特性,現在問題來了,如果迭代器的next方法中有非同步操作,該怎麼辦呢?ES6提供Promise作為非同步處理的標準API,因此可以用個別的Promise實例來包裹每次的非同步操作,也就是說,每次呼叫迭代器next方法時,結果物件的value特性會是Promise,在then方法指定的回呼函式中,才能取得結果物件,就迭代器的實作來說,並不直覺(可參考https://bit.ly/2J4JddY)。

搭配for-of語法時,切記不能是for(let v of vertex1) { console.log(v); }這類操作,因v會是Promise,須進一步在then方法指定回呼函式,例如:

for(let p of vertex1) {
p.then(v => console.log(vo));
}

ES8的async、await

Promise模式是基於API層面的風格,在盛行了一段時間後,為了撰寫程式碼時,更方便與直覺,ES8新增了async、await作為語法層面的支援。async函式實作內容看來與一般函式無異,然而return的值會被包裝在Promise中,作為呼叫async函式後的傳回值,await必須在一個async函式中使用,await後接上一個Promise,會對Promise作解析,將resolve的值作為傳回值,解析未完成前會阻斷流程,因此,對於一個async函式foo,原本使用Promise的寫法若是foo().then(v => doSomething(v)),可以改寫為:

async function main() {
doSomething(await foo());
}
main();

程式碼更為簡潔,風格看來也像是同步風格,閱讀時較為直覺,執行main()仍具有非同步的效果。不過,在逐漸熟悉async、await之後,想進一步套用到更多非同步情境時,就會遇上問題,例如,async函式實作中若有迭代流程時該怎麼辦呢?該怎麼return值?

就結論而言,沒辦法直接使用async函式實作,必須如方才實作Symbol.iterator協定(可參考https://bit.ly/2VgUXkl),至於搭配for-of迴圈的部份,可以用await來取代Promise的then,例如在一個async函式中,執行for(let p of vertex1) { console.log(await p); }。

ES6的Symbol.iterator協定,就語義上來說,其實是同步迭代,只不過這邊被迫運用在非同步情境,而造成實作上的麻煩,閱讀程式碼時非同步的語義也不明確,因此,在ES9中,增加了非同步迭代協定、for-await-of相關語法。

ES9非同步迭代協定、for-await-of

ES6中透過function*與yield定義的函式,執行後傳回的是產生器,也就是Generator實例,具有Symbol.iterator特性以及next方法(因此也是個迭代器),如果在function*前加上async呢?

到了ES9,允許這麼做,也可以在函式中yield值,函式執行後會傳回非同步產生器,也就是AsyncGenerator實例,具有Symbol.asyncIterator特性以及next方法,next方法傳回Promise,而不是具有value、done特性的結果物件。

開發者可以試著只使用function*而不加上async,實作出具有與async function*相同效果的函式,這需要在function*內部包含一個function*,呼叫後者得到產生器後,進行迭代,並將next結果作為Promise的resolve值(可參考https://bit.ly/2VhM9dO)。

這會令人聯想到Python 3.3的yield from。在Python中,這是用來銜接產生器,以直接yield其結果的簡便語法,但yield from後來被Python廢棄,而ES9顯然記取這個經驗,直接新增Symbol.asyncIterator協定,相當於Python 3.6時新增的__aiter__協定。

ES9中具有Symbol.asyncIterator協定的物件是「可非同步迭代的」,該特性參考的函式,使用async function*實作會更為方便直覺;只有Symbol.asyncIterator協定的物件,不能搭配for-of使用(這搭配的是Symbol.iterator協定),必須搭配ES9新增的for-await-of來使用,例如,若vertex2是可非同步迭代的物件(可參考https://bit.ly/2VjOk0K),就可以在一個async函式中如下撰寫程式:

for await (const x of vertex2) {
console.log(x);
}

有些談及for-await-of的文件中,會出現過於簡化的說明,通常是以一個promises陣列為例,談到for(const v of promises) { console.log(v); }無法取得結果,因為v是個Promise,必須得是for await(const v of promises) { console.log(v); }才能取得resolve後的值,就結論而言,這並不能說是錯,然而,千萬要記得的是,Symbol.iterator與Symbol.asyncIterator是不同的協定。

promises陣列可以運作的原理在於,for-of語法只認Symbol.iterator協定,for-await-of可以使用Symbol.iterator與Symbol.asyncIterator協定,然而只有Symbol.asyncIterator協定的物件,不能用於for-of!

同步與非同步情境有所不同

如果想從更多程式碼來瞭解ES9非同步迭代的細節,可以參考〈ES2018: asynchronous iteration〉(https://bit.ly/2PRNsua),若開發者亦熟悉Python,從中可以看到Python中async for的影子。

無論是Python或JavaScript,對於非同步迭代的發展過程的重點,都在於,非同步迭代並非只是語法上的不同,而是不同情境下使用不同的角色,才能在意圖上突顯目前是在進行同步或是非同步操作,雖然Promise搭配同步協定也可以解決事情,然而若執行環境支援,使用Symbol.asyncIterator協定、for-await-of,就程式意圖來說,會是更好的表示方式!

作者簡介


Advertisement

更多 iThome相關內容