本文是“一個 JSer 的 Dart 學習日誌”系列的第四篇,本系列文章主要以挖掘 JS 與 Dart 異同點的方式,在複習和鞏固 JS 的同時平穩地過渡到 Dart 語言。
鑑於作者尚屬 Dart 初學者,所以認識可能會比較膚淺和片面,如您慧眼識蟲,希望不吝指正。
如無特殊說明,本文中 JS 包含了自 ES5 至 ES2021 的全部特性, Dart 版本則為 2.0 以上版本。Google 原本是將 Dart 作為 JS 的繼任者來開發的,因此在設計上借鑑了許多 JS 的特性,例如事件驅動和單執行緒,這使得它們的非同步程式設計寫法也十分相似。
一. 使用回撥函式
共同之處
無論是 JS 還是 Dart ,都秉承著“一切皆物件”的理念,函式概莫能外,將一個函式作為引數傳遞,以期在適宜時機呼叫,是最簡單的非同步程式設計方案,此函式即回撥函式。
> /* ******************** Both JS and Dart ******************** */ > var func = (param) => param + 1; > var caller = (callback) => callback(1);
不同之處
- Dart 的非同步過程與 JS 不一樣——它是阻塞的,僅靠回撥函式並不能解決問題,因此回撥函式風格的非同步介面是 JS 的特色(就這兩門語言而言)。
Dart 特有的
1. 回撥函式的型別
在定義一個函式的時候,回撥函式是其引數之一引數,而函式的引數是可以宣告型別的,回撥函式概莫能外。不過由於函式的要素較多,與其他型別的宣告方式不一樣——
其他型別:型別關鍵字在變數名前面:void func(int a, Map<String, dynamic> b){ // Do something }
。
函式型別:函式的型別是Function
,但宣告形參型別為函式的語法並不是Function callback
:// callback 第一個引數為 `num` 型別,第二個引數為 `int`型別 // callback 的返回值為 `String` 型別 // 如果這些型別都未顯式宣告的話,則全部都是 dynamic void func(String callback(num a, int b)){ // Do something }
二. 使用生成器函式
ES6 加入了生成器函式,可以暫時中斷執行,並在“喚醒”之後繼續執行,這個特性與非同步過程相得益彰,因此生成器函式也被用於處理非同步過程。
Dart 中也有生成器函式,在概念上與 JS 生成器類似,但在語法和使用方式上有很多不同。
共同之處
1. 使用 *
和 yeild
使用
*
宣告一個生成器函式,使用yield
關鍵字暫停函式,並生成值。> /* JS */ | /* Dart */ > function* gen(max) { | gen(int max) sync* { > var state = 0; | var state = 0; > while(state < max) { | while(state < max) { > yield state ++; | yeild state ++; > } | } > } | } > const reader = gen(6); | final reader = gen(6); > console.log(reader.next().value); | print('${render.elementAt(0)}'); > // 0 | // 0
生成器函式適用於一些重複觸發呼叫的場景,例如
WebSocket
的介面事件。
2. “惰性”求值
- 生成器函式執行遇到
yield
即中止,並返回一個特定物件,呼叫此物件的特定方法,函式才會繼續執行,直到遇到下一個yield
。
不同之處
1. Dart 生成器有同步與非同步之分
- JS 只有一種生成器,返回的可迭代物件都是
Generator
; - Dart 的生成器分為同步(
sync
)與非同步(async
),同步生成器函式返回Iterator
例項,非同步生成器返回Stream
例項。
三. Future
VS Promise
共同之處
1. 包裝 async
函式的返回值
雖然非同步程式設計的歷史還算悠久,但非同步函式卻是一個年輕的概念,所以在程式語言中引入非同步函式的第一個問題就是:如何將非同步函式嵌入到同步的邏輯流裡?
對於這個問題,JS 給出的答案是 Promise
,相應地,Dart 的答案是 Future
:
> /* JS */ | // Dart
> async function asyncFunc() { | asyncFunc() async {
> return await 'yes'; | return await 'yes';
> } | }
> console.log(asyncFunc()); | print(asyncFunc());
> // Promise {<pending>} | // Instance of '_Future<dynamic>'
2. .then
方法和鏈式呼叫
兩種方案都使用 .then
語法訂閱非同步過程的最終結果:
> /* JS */ | // Dart
> asyncFunc().then( | asyncFunc().then(
> (res) => | (res) =>
> console.log(`Got ${res}`) | print('Got $res')
> ) | )
> // Got yes | // Got yes
。
並且,.then
方法也會返回一個新的Promise/Future
,訂閱此返回值可以獲得回撥函式的返回值(或回撥函式返回的Promise/Future
包裝的值):
> /* JS */ | // Dart
> async function plus1(v = 0) { | int plus1(int v) async {
> return await v + 1; | return await v + 1;
> } | }
> function plus2(v = 0) { | int plus2(int v) {
> return v + 2; | return v + 2;
> } | }
> plus1().then(plus1) | plus1().then(plus1)
> .then(plus2).then(console.log); | .then(plus2).then(print);
> // 4 | // 4
不同之處
1. Dart 型別標註
在本系列文章中,Dart 的這個特點算是老生常談了,Dart
中使用泛型語法約束Future
及其所包裝的值的型別:
Future<int> foo async {
return 1;
}
2. Promise.all
vs Future.wait
這個一時不知道該算共同點還是不同點,因為語法完全一致,僅僅是關鍵字不同而已:
> /* JS */ | // Dart
> Promise.all([ | Future.wait([
> plus1(), | plus1(),
> plus1() | plus1()
> ]).then( | ]).then(
> () => console.log('All done'); | () => print('All done');
> ); | );
3. 建構函式的引數不同
傳入的函式引數形式不一樣
二者都需要傳入一個函式,但是這個函式的形式不太一樣。
Promise
的excutor
有兩個位置引數:resolve
和reject
。Promise
所“包裝”的值即resolve
函式的返回值;Future
的computation
函式則沒有引數,Future
所包裝的正是computation
的返回值。> /* JS */ | // Dart > const a = new Promise( | final a = /*new*/ Future( > (resolve, reject) => resolve(1) | () => 1 > ); | ); > console.log(await a); | print(await a); > // 1 | // 1
computation
預設非同步執行
Promise
的excutor
用來初始化Promise
,並且 JS 的非同步過程不會阻塞,所以是同步執行的;Future
的computation
直接用來獲取值,是非同步執行的:> /* JS */ | // Dart > var mut = 0; | var mut = 0; > const a = new Promise( | final a = /*new*/ Future( > function (resolve, reject) { | () { > mut++; | mut++; > resolve(1); | return 1; > } | } > ); | ); > console.log(mut); | print(mut); > // 1 | // 0
;
如果想同步執行
computation
,應使用命名建構函式Future.sync
:int mut = 0; final a = Future.sync((){ mut++; return mut; }); print(mut); // 1
4. 包裝值與錯誤
- JS 使用
Promise.resolve(value)
將value
包裝在一個Promise
中,用Promise.reject(error)
包裝錯誤error
; - Dart 的
Future.value(value)
和Future.error(error)
分別實現上述功能。
其實我不知道這兩種包裝有什麼用。
5. Future
承擔了更多非同步程式設計的任務
Future.delayed
VS window.setTimeout
- JS 使用頂層物件提供的
setTimeout
介面註冊延時任務,這是一個回撥風格的介面; Dart 使用命名建構函式
Future.delayed
註冊延時任務:> /* JS */ | // Dart > var task = setTimeout( | var task = Future.delayed( > () => { | Duration(milliseconds: 100), > console.log('done'); | () { > }, | print('done'); > 100 | } > }; | };
Dart 使用
Duration
類來構造時間差,比 JS 預設的毫秒數要直觀得多(但是寫起來多少有點麻煩,不知道有沒有語法糖)。
Future.microstack
VS Promise.resolve().then
- JS 中註冊微任務最便捷的方案是
Promise.resolve().then
,(當然,前提是使用執行時提供的Promise
或者靠譜的polyfill
方案),雖然“便捷”,但畢竟只是一種 trick; 而 Dart 提供了專門的介面
Future.microtask
來註冊微任務:> /* JS */ | // Dart > function register(task){ | register(task){ > Promise.resolve.then( | Future.microtask( > task | task > ); | ); > } | }
好在絕大多數情況下,普通的開發者不需要開發者自己排程任務優先順序,因此 JS 的這個寫法無關緊要,只要在面試的時候不要掉鏈子就行。
6. Promise
有更多豐富的功能
熟悉
Promise
的人不會對Promise.allSettle
、Promise.race
、Promise.any
這些靜態方法感到陌生,而這些方法是Future
所不具有的,希望早日能在 Dart 裡見到它們。JS 總算扳回一局!
四. async/await
如果你問我最喜歡自ES6
以來加入的哪個新特性,那毫無疑問是ES2017
帶來的async/await
語法和ES2015
帶來的解構語法。
而在 Dart 中,async/await
這一神兵利器也沒有缺席!
7. Future
是dart:async
包提供的功能
如欲使用
Future
(以及),應當先引入dart:async
包。然而在 Dartpad 中不引入也可以使用。
相同之處
用法基本相似
Talk is cheap, here is the code:
> /* JS */ | // Dart > async function foo(){ | foo () async { > return await asyncFunc(); | return await asyncFunc(); > } | {
不同之處
1. async
關鍵字的位置
- 在 JS 中,
async
置於函式宣告語句的前面; 在 Dart 中,
async
置於函式引數列表的後面。這個區別在上面的例子中已經有所體現。
TODO: 需要稍微研究下 Dart 建構函式初始化例項變數的時候,async
放哪裡。所以這裡總結的位置不一定是對的。
2. 分別返回Promise
和Future
- 在 JS 中,
async
函式返回Promise
例項; - 在 Dart 中,
async
函式返回Future
例項。
兩種類的差異在上一節已經闡明(至少作者自己覺得是闡明瞭),因此不再贅述。