Flutter/Dart中的非同步

ad6623發表於2019-01-30

前言

我們所熟悉的前端開發框架大都是事件驅動的。事件驅動意味著你的程式中必然存在事件迴圈和事件佇列。事件迴圈會不停的從事件佇列中獲取和處理各種事件。也就是說你的程式必然是支援非同步的。

在Android中這樣的結構是Looper/Handler;在iOS中是RunLoop;在JavaScript中是Event Loop。

同樣的Flutter/Dart也是事件驅動的,也有自己的Event Loop。而且這個Event Loop和JavaScript的很像,很像。(畢竟Dart是想替換JS來著)。下面我們就來了解一下Dart中的Event Loop。

Dart的Event Loop

Dart的事件迴圈如下圖所示。和JavaScript的基本一樣。迴圈中有兩個佇列。一個是微任務佇列(MicroTask queue),一個是事件佇列(Event queue)。

  • 事件佇列包含外部事件,例如I/O, Timer,繪製事件等等。
  • 微任務佇列則包含有Dart內部的微任務,主要是通過scheduleMicrotask來排程。

Dart的Event Loop

Dart的事件迴圈的執行遵循以下規則:

  • 首先處理所有微任務佇列裡的微任務。
  • 處理完所有微任務以後。從事件佇列裡取1個事件進行處理。
  • 回到微任務佇列繼續迴圈。

注意第一步裡的所有,也就是說在處理事件佇列之前,Dart要先把所有的微任務處理完。如果某一時刻微任務佇列裡有8個微任務,事件佇列有2個事件,Dart也會先把這8個微任務全部處理完再從事件佇列中取出1個事件處理,之後又會回到微任務佇列去看有沒有未執行的微任務。

總而言之,就是對微任務佇列是一次性全部處理,對於事件佇列是一次只處理一個。

這個流程要清楚,清楚了才能理解Dart程式碼的執行順序。

非同步執行

那麼在Dart中如何讓你的程式碼非同步執行呢?很簡單,把要非同步執行的程式碼放在微任務佇列或者事件佇列裡就行了。

  • 可以呼叫scheduleMicrotask來讓程式碼以微任務的方式非同步執行
    scheduleMicrotask((){
        print('a microtask');
    });
複製程式碼
  • 可以呼叫Timer.run來讓程式碼以Event的方式非同步執行
   Timer.run((){
       print('a event');
   });
複製程式碼

好了,現在你知道怎麼讓你的Dart程式碼非同步執行了。看起來並不是很複雜,但是你需要清楚的知道你的非同步程式碼執行的順序。這也是很多前端面試時候會問到的問題。舉個簡單的例子,請問下面這段程式碼是否會輸出"executed"?

main() {
     Timer.run(() { print("executed"); });  
      foo() {
        scheduleMicrotask(foo);  
      }
      foo();
    }
複製程式碼

答案是不會,因為在始終會有一個foo存在於微任務佇列。導致Event Loop沒有機會去處理事件佇列。還有更復雜的一些例子會有大量的非同步程式碼混合巢狀起來然後問你執行順序是什麼樣的,這都需要按照上述Event Loop規則仔細去分析。

和JS一樣,僅僅使用回撥函式來做非同步的話很容易陷入“回撥地獄(Callback hell)”,為了避免這樣的問題,JS引入了Promise。同樣的, Dart引入了Future

Future

要使用Future的話需要引入dart.async

import 'dart:async';
複製程式碼

Future提供了一系列建構函式供你選擇。

建立一個立刻在事件佇列裡執行的Future:

Future(() => print('立刻在Event queue中執行的Future'));
複製程式碼

建立一個延時1秒在事件佇列裡執行的Future:

Future.delayed(const Duration(seconds:1), () => print('1秒後在Event queue中執行的Future'));
複製程式碼

建立一個在微任務佇列裡執行的Future:

Future.microtask(() => print('在Microtask queue裡執行的Future'));
複製程式碼

建立一個同步執行的Future:

Future.sync(() => print('同步執行的Future'));
複製程式碼

對,你沒看錯,同步執行的。

這裡要注意一下,這個同步執行指的是構造Future的時候傳入的函式是同步執行的,這個Future通過then串進來的回撥函式是排程到微任務佇列非同步執行的。

有了Future之後, 通過呼叫then來把回撥函式串起來,這樣就解決了"回撥地獄"的問題。

Future(()=> print('task'))
    .then((_)=> print('callback1'))
    .then((_)=> print('callback2'));
複製程式碼

在task列印完畢以後,通過then串起來的回撥函式會按照連結的順序依次執行。 如果task執行出錯怎麼辦?你可以通過catchError來鏈上一個錯誤處理函式:

 Future(()=> throw 'we have a problem')
      .then((_)=> print('callback1'))
      .then((_)=> print('callback2'))
      .catchError((error)=>print('$error'));
複製程式碼

上面這個Future執行時直接丟擲一個異常,這個異常會被catchError捕捉到。類似於Java中的try/catch機制的catch程式碼塊。執行後只會執行catchError裡的程式碼。兩個then中的程式碼都不會被執行。

既然有了類似Java的try/catch,那麼Java中的finally也應該有吧。有的,那就是whenComplete:

Future(()=> throw 'we have a problem')
    .then((_)=> print('callback1'))
    .then((_)=> print('callback2'))
    .catchError((error)=>print('$error'))
    .whenComplete(()=> print('whenComplete'));
複製程式碼

無論這個Future是正常執行完畢還是丟擲異常,whenComplete都一定會被執行。

以上就是對Future的一些主要用法的介紹。Future背後的實現機制還是有一些複雜的。這裡先列幾個來自Dart官網的關於Future的燒腦說明。大家先感受一下:

  1. 你通過then串起來的那些回撥函式在Future完成的時候會被立即執 行,也就是說它們是同步執行,而不是被排程非同步執行。
  2. 如果Future在呼叫then串起回撥函式之前已經完成,
    那麼這些回撥函式會被排程到微任務佇列非同步執行。
  3. 通過Future()Future.delayed()例項化的Future不會同步執行,它們會被排程到事件佇列非同步執行。
  4. 通過Future.value()例項化的Future會被排程到微任務佇列非同步完成,類似於第2條。
  5. 通過Future.sync()例項化的Future會同步執行其入參函式,然後(除非這個入參函式返回一個Future)排程到微任務佇列來完成自己,類似於第2條。

從上述說明可以得出結論,Future中的程式碼至少會有一部分被非同步排程執行的,要麼是其入參函式和回撥被非同步排程執行,要麼就只有回撥被非同步排程執行。

不知道大家注意到沒有,通過以上那些Future建構函式生成的Future物件其實控制權不在你這裡。它什麼時候執行完畢只能等系統排程了。你只能被動的等待Future執行完畢然後呼叫你設定的回撥。如果你想手動控制某個Future怎麼辦呢?請使用Completer

Completer

這裡就舉個Completer的例子吧

// 例項化一個Completer
var completer = Completer();
// 這裡可以拿到這個completer內部的Future
var future = completer.future;
// 需要的話串上回撥函式。
future.then((value)=> print('$value'));

//做些其它事情 
...
// 設定為完成狀態
completer.complete("done");

複製程式碼

上述程式碼片段中,當你建立了一個Completer以後,其內部會包含一個Future。你可以在這個Future上通過then, catchErrorwhenComplete串上你需要的回撥。拿著這個Completer例項,在你的程式碼裡的合適位置,通過呼叫complete函式即可完成這個Completer對應的Future。控制權完全在你自己的程式碼手裡。當然你也可以通過呼叫completeError來以異常的方式結束這個Future

總結就是:

  • 我建立的,完成了調我的回撥就行了: 用 Future
  • 我建立的,得我來結束它: 用Completer

Future相對於排程回撥函式來說,緩減了回撥地獄的問題。但是如果Future要串起來的的東西比較多的話,程式碼還是會可讀性比較差。特別是各種Future巢狀起來,是比較燒腦的。

所以能不能更給力一點呢?可以的!JavaScript有 async/await,Dart也有。

async/await

asyncawait是什麼?它們是Dart語言的關鍵字,有了這兩個關鍵字,可以讓你用同步程式碼的形式寫出非同步程式碼。啥意思呢?看下面這個例子:

foo() async {
  print('foo E');
  String value = await bar();
  print('foo X $value');
}

bar() async {
  print("bar E");
  return "hello";
}

main() {
  print('main E');
  foo();
  print("main X");
}
複製程式碼

函式foo被關鍵字async修飾,其內部的有3行程式碼,看起來和普通的函式沒什麼兩樣。但是在第2行等號右側有個await關鍵字,await的出現讓看似會同步執行的程式碼裂變為兩部分。如下圖所示:

async await
綠框裡面的程式碼會在foo函式被呼叫的時候同步執行,在遇到await的時候,會馬上返回一個Future,剩下的紅框裡面的程式碼以then的方式鏈入這個Future被非同步排程執行。

上述程式碼執行以後在終端會輸出如下:

output
可見print('foo X $value')是在main執行完畢以後才列印出來的。的確是非同步執行的。

而以上程式碼中的foo函式可以以Future方式實現如下,兩者是等效的

foo() {
  print('foo E');
  return Future.sync(bar).then((value) => print('foo X $value'));
}
複製程式碼

await並不像字面意義上程式執行到這裡就停下來啥也不幹等待Future完成。而是立刻結束當前函式的執行並返回一個Future。函式內剩餘程式碼通過排程非同步執行。

  • await只能在async函式中出現。
  • async函式中可以出現多個await,每遇見一個就返回一個Future, 實際結果類似於用then串起來的回撥。
  • async函式也可以沒有await, 在函式體同步執行完畢以後返回一個Future

使用asyncawait還有一個好處是我們可以用和同步程式碼相同的try/catch機制來做異常處理。

foo() async {
  try {
    print('foo E');
    var value = await bar();
    print('foo X $value');
  } catch (e) {
    // 同步執行程式碼中的異常和非同步執行程式碼的異常都會被捕獲
  } finally {
    
  }
}
複製程式碼

在日常使用場景中,我們通常利用asyncawait來非同步處理IO,網路請求,以及Flutter中的Platform channels通訊等耗時操作。

總結

本文大致介紹了Flutter/Dart中的非同步執行機制,從非同步執行的基礎(Event Loop)開始,首先介紹了最原始的非同步執行機制,直接排程回撥函式;到Future;再到 asyncawait。瞭解了Flutter/Dart中的非同步執行機制是如何一步一步的進化而來的。對於一直從事Native開發,不太瞭解JavaScrip的同學來講,這個非同步機制和原生開發有很大的不同,需要多多動手練習,動腦思考才能適應。本文中介紹的相關知識點較為粗淺,並沒有涉及dart:async中關於Future實現的原始碼分析以及Stream等不太常用的類。這些如果大家想了解一下的話我會另寫文章來介紹一下。

相關文章