Flutter/Dart中的異步

前言

咱們所熟悉的前端開發框架大都是事件驅動的。事件驅動意味着你的程序中必然存在事件循環和事件隊列。事件循環會不停的從事件隊列中獲取和處理各類事件。也就是說你的程序必然是支持異步的。前端

在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個事件處理,以後又會回到微任務隊列去看有沒有未執行的微任務。async

總而言之,就是對微任務隊列是一次性所有處理,對於事件隊列是一次只處理一個。函數

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

異步執行

那麼在Dart中如何讓你的代碼異步執行呢?很簡單,把要異步執行的代碼放在微任務隊列或者事件隊列裏就好了。源碼分析

  • 能夠調用scheduleMicrotask來讓代碼以微任務的方式異步執行
scheduleMicrotask((){
        print('a microtask');
    });
複製代碼
  • 能夠調用Timer.run來讓代碼以Event的方式異步執行
Timer.run((){
       print('a event');
   });
複製代碼

好了,如今你知道怎麼讓你的Dart代碼異步執行了。看起來並非很複雜,可是你須要清楚的知道你的異步代碼執行的順序。這也是不少前端面試時候會問到的問題。舉個簡單的例子,請問下面這段代碼是否會輸出"executed"?ui

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等不太經常使用的類。這些若是你們想了解一下的話我會另寫文章來介紹一下。

相關文章
相關標籤/搜索