一個 JSer 的 Dart 學習日誌(四):非同步程式設計

知名噴子發表於2021-10-09

本文是“一個 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. 建構函式的引數不同

傳入的函式引數形式不一樣

二者都需要傳入一個函式,但是這個函式的形式不太一樣。
  • Promiseexcutor 有兩個位置引數:resolverejectPromise所“包裝”的值即resolve函式的返回值;
  • Futurecomputation 函式則沒有引數,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 預設非同步執行

  • Promiseexcutor 用來初始化 Promise,並且 JS 的非同步過程不會阻塞,所以是同步執行的;
  • Futurecomputation 直接用來獲取值,是非同步執行的:

    > /*  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.allSettlePromise.racePromise.any 這些靜態方法感到陌生,而這些方法是 Future 所不具有的,希望早日能在 Dart 裡見到它們。

    JS 總算扳回一局!

四. async/await

如果你問我最喜歡自ES6以來加入的哪個新特性,那毫無疑問是ES2017帶來的async/await語法和ES2015帶來的解構語法。
而在 Dart 中,async/await這一神兵利器也沒有缺席!

7. Futuredart: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. 分別返回PromiseFuture

  • 在 JS 中,async函式返回Promise例項;
  • 在 Dart 中,async函式返回Future例項。
兩種類的差異在上一節已經闡明(至少作者自己覺得是闡明瞭),因此不再贅述。

相關文章