Flutter/Dart中的異步

前言

我們所熟悉的前端開發(fā)框架大都是事件驅(qū)動(dòng)的。事件驅(qū)動(dòng)意味著你的程序中必然存在事件循環(huán)和事件隊(duì)列。事件循環(huán)會(huì)不停的從事件隊(duì)列中獲取和處理各種事件。也就是說你的程序必然是支持異步的。

在Android中這樣的結(jié)構(gòu)是Looper/Handler;在iOS中是RunLoop;在JavaScript中是Event Loop。

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

Dart的Event Loop

Dart的事件循環(huán)如下圖所示。和JavaScript的基本一樣。循環(huán)中有兩個(gè)隊(duì)列。一個(gè)是微任務(wù)隊(duì)列(MicroTask queue),一個(gè)是事件隊(duì)列(Event queue)。

  • 事件隊(duì)列包含外部事件,例如I/O, Timer,繪制事件等等。
  • 微任務(wù)隊(duì)列則包含有Dart內(nèi)部的微任務(wù),主要是通過scheduleMicrotask來調(diào)度。
Dart的Event Loop

Dart的事件循環(huán)的運(yùn)行遵循以下規(guī)則:

  • 首先處理所有微任務(wù)隊(duì)列里的微任務(wù)。
  • 處理完所有微任務(wù)以后。從事件隊(duì)列里取1個(gè)事件進(jìn)行處理。
  • 回到微任務(wù)隊(duì)列繼續(xù)循環(huán)。

注意第一步里的所有,也就是說在處理事件隊(duì)列之前,Dart要先把所有的微任務(wù)處理完。如果某一時(shí)刻微任務(wù)隊(duì)列里有8個(gè)微任務(wù),事件隊(duì)列有2個(gè)事件,Dart也會(huì)先把這8個(gè)微任務(wù)全部處理完再從事件隊(duì)列中取出1個(gè)事件處理,之后又會(huì)回到微任務(wù)隊(duì)列去看有沒有未執(zhí)行的微任務(wù)。

總而言之,就是對(duì)微任務(wù)隊(duì)列是一次性全部處理,對(duì)于事件隊(duì)列是一次只處理一個(gè)。

這個(gè)流程要清楚,清楚了才能理解Dart代碼的執(zhí)行順序。

異步執(zhí)行

那么在Dart中如何讓你的代碼異步執(zhí)行呢?很簡單,把要異步執(zhí)行的代碼放在微任務(wù)隊(duì)列或者事件隊(duì)列里就行了。

  • 可以調(diào)用scheduleMicrotask來讓代碼以微任務(wù)的方式異步執(zhí)行
    scheduleMicrotask((){
        print('a microtask');
    });
  • 可以調(diào)用Timer.run來讓代碼以Event的方式異步執(zhí)行
   Timer.run((){
       print('a event');
   });

好了,現(xiàn)在你知道怎么讓你的Dart代碼異步執(zhí)行了。看起來并不是很復(fù)雜,但是你需要清楚的知道你的異步代碼執(zhí)行的順序。這也是很多前端面試時(shí)候會(huì)問到的問題。舉個(gè)簡單的例子,請(qǐng)問下面這段代碼是否會(huì)輸出"executed"?

main() {
     Timer.run(() { print("executed"); });  
      foo() {
        scheduleMicrotask(foo);  
      }
      foo();
    }

答案是不會(huì),因?yàn)樵谑冀K會(huì)有一個(gè)foo存在于微任務(wù)隊(duì)列。導(dǎo)致Event Loop沒有機(jī)會(huì)去處理事件隊(duì)列。還有更復(fù)雜的一些例子會(huì)有大量的異步代碼混合嵌套起來然后問你執(zhí)行順序是什么樣的,這都需要按照上述Event Loop規(guī)則仔細(xì)去分析。

和JS一樣,僅僅使用回調(diào)函數(shù)來做異步的話很容易陷入“回調(diào)地獄(Callback hell)”,為了避免這樣的問題,JS引入了Promise。同樣的, Dart引入了Future

Future

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

import 'dart:async';

Future提供了一系列構(gòu)造函數(shù)供你選擇。

創(chuàng)建一個(gè)立刻在事件隊(duì)列里運(yùn)行的Future:

Future(() => print('立刻在Event queue中運(yùn)行的Future'));

創(chuàng)建一個(gè)延時(shí)1秒在事件隊(duì)列里運(yùn)行的Future:

Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中運(yùn)行的Future'));

創(chuàng)建一個(gè)在微任務(wù)隊(duì)列里運(yùn)行的Future:

Future.microtask(() => print('在Microtask queue里運(yùn)行的Future'));

創(chuàng)建一個(gè)同步運(yùn)行的Future:

Future.sync(() => print('同步運(yùn)行的Future'));

對(duì),你沒看錯(cuò),同步運(yùn)行的。

這里要注意一下,這個(gè)同步運(yùn)行指的是構(gòu)造Future的時(shí)候傳入的函數(shù)是同步運(yùn)行的,這個(gè)Future通過then串進(jìn)來的回調(diào)函數(shù)是調(diào)度到微任務(wù)隊(duì)列異步執(zhí)行的。

有了Future之后, 通過調(diào)用then來把回調(diào)函數(shù)串起來,這樣就解決了"回調(diào)地獄"的問題。

Future(()=> print('task'))
    .then((_)=> print('callback1'))
    .then((_)=> print('callback2'));

在task打印完畢以后,通過then串起來的回調(diào)函數(shù)會(huì)按照鏈接的順序依次執(zhí)行。
如果task執(zhí)行出錯(cuò)怎么辦?你可以通過catchError來鏈上一個(gè)錯(cuò)誤處理函數(shù):

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

上面這個(gè)Future執(zhí)行時(shí)直接拋出一個(gè)異常,這個(gè)異常會(huì)被catchError捕捉到。類似于Java中的try/catch機(jī)制的catch代碼塊。運(yùn)行后只會(huì)執(zhí)行catchError里的代碼。兩個(gè)then中的代碼都不會(huì)被執(zhí)行。

既然有了類似Java的try/catch,那么Java中的finally也應(yīng)該有吧。有的,那就是whenComplete:

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

無論這個(gè)Future是正常執(zhí)行完畢還是拋出異常,whenComplete都一定會(huì)被執(zhí)行。

以上就是對(duì)Future的一些主要用法的介紹。Future背后的實(shí)現(xiàn)機(jī)制還是有一些復(fù)雜的。這里先列幾個(gè)來自Dart官網(wǎng)的關(guān)于Future的燒腦說明。大家先感受一下:

  1. 你通過then串起來的那些回調(diào)函數(shù)在Future完成的時(shí)候會(huì)被立即執(zhí) 行,也就是說它們是同步執(zhí)行,而不是被調(diào)度異步執(zhí)行。
  2. 如果Future在調(diào)用then串起回調(diào)函數(shù)之前已經(jīng)完成,
    那么這些回調(diào)函數(shù)會(huì)被調(diào)度到微任務(wù)隊(duì)列異步執(zhí)行。
  3. 通過Future()Future.delayed()實(shí)例化的Future不會(huì)同步執(zhí)行,它們會(huì)被調(diào)度到事件隊(duì)列異步執(zhí)行。
  4. 通過Future.value()實(shí)例化的Future會(huì)被調(diào)度到微任務(wù)隊(duì)列異步完成,類似于第2條。
  5. 通過Future.sync()實(shí)例化的Future會(huì)同步執(zhí)行其入?yún)⒑瘮?shù),然后(除非這個(gè)入?yún)⒑瘮?shù)返回一個(gè)Future)調(diào)度到微任務(wù)隊(duì)列來完成自己,類似于第2條。

從上述說明可以得出結(jié)論,Future中的代碼至少會(huì)有一部分被異步調(diào)度執(zhí)行的,要么是其入?yún)⒑瘮?shù)和回調(diào)被異步調(diào)度執(zhí)行,要么就只有回調(diào)被異步調(diào)度執(zhí)行。

不知道大家注意到?jīng)]有,通過以上那些Future構(gòu)造函數(shù)生成的Future對(duì)象其實(shí)控制權(quán)不在你這里。它什么時(shí)候執(zhí)行完畢只能等系統(tǒng)調(diào)度了。你只能被動(dòng)的等待Future執(zhí)行完畢然后調(diào)用你設(shè)置的回調(diào)。如果你想手動(dòng)控制某個(gè)Future怎么辦呢?請(qǐng)使用Completer

Completer

這里就舉個(gè)Completer的例子吧

// 實(shí)例化一個(gè)Completer
var completer = Completer();
// 這里可以拿到這個(gè)completer內(nèi)部的Future
var future = completer.future;
// 需要的話串上回調(diào)函數(shù)。
future.then((value)=> print('$value'));

//做些其它事情 
...
// 設(shè)置為完成狀態(tài)
completer.complete("done");

上述代碼片段中,當(dāng)你創(chuàng)建了一個(gè)Completer以后,其內(nèi)部會(huì)包含一個(gè)Future。你可以在這個(gè)Future上通過then, catchErrorwhenComplete串上你需要的回調(diào)。拿著這個(gè)Completer實(shí)例,在你的代碼里的合適位置,通過調(diào)用complete函數(shù)即可完成這個(gè)Completer對(duì)應(yīng)的Future。控制權(quán)完全在你自己的代碼手里。當(dāng)然你也可以通過調(diào)用completeError來以異常的方式結(jié)束這個(gè)Future

總結(jié)就是:

  • 我創(chuàng)建的,完成了調(diào)我的回調(diào)就行了: 用 Future
  • 我創(chuàng)建的,得我來結(jié)束它: 用Completer

Future相對(duì)于調(diào)度回調(diào)函數(shù)來說,緩減了回調(diào)地獄的問題。但是如果Future要串起來的的東西比較多的話,代碼還是會(huì)可讀性比較差。特別是各種Future嵌套起來,是比較燒腦的。

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

async/await

asyncawait是什么?它們是Dart語言的關(guān)鍵字,有了這兩個(gè)關(guān)鍵字,可以讓你用同步代碼的形式寫出異步代碼。啥意思呢?看下面這個(gè)例子:

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");
}

函數(shù)foo被關(guān)鍵字async修飾,其內(nèi)部的有3行代碼,看起來和普通的函數(shù)沒什么兩樣。但是在第2行等號(hào)右側(cè)有個(gè)await關(guān)鍵字,await的出現(xiàn)讓看似會(huì)同步執(zhí)行的代碼裂變?yōu)閮刹糠帧H缦聢D所示:

async await

綠框里面的代碼會(huì)在foo函數(shù)被調(diào)用的時(shí)候同步執(zhí)行,在遇到await的時(shí)候,會(huì)馬上返回一個(gè)Future,剩下的紅框里面的代碼以then的方式鏈入這個(gè)Future被異步調(diào)度執(zhí)行。

上述代碼運(yùn)行以后在終端會(huì)輸出如下:

output

可見print('foo X $value')是在main執(zhí)行完畢以后才打印出來的。的確是異步執(zhí)行的。

而以上代碼中的foo函數(shù)可以以Future方式實(shí)現(xiàn)如下,兩者是等效的

foo() {
  print('foo E');
  return Future(bar).then((value) => print('foo X $value'));
}

await并不像字面意義上程序運(yùn)行到這里就停下來啥也不干等待Future完成。而是立刻結(jié)束當(dāng)前函數(shù)的執(zhí)行并返回一個(gè)Future。函數(shù)內(nèi)剩余代碼通過調(diào)度異步執(zhí)行。

  • await只能在async函數(shù)中出現(xiàn)。
  • async函數(shù)中可以出現(xiàn)多個(gè)await,每遇見一個(gè)就返回一個(gè)Future, 實(shí)際結(jié)果類似于用then串起來的回調(diào)。
  • async函數(shù)也可以沒有await, 在函數(shù)體同步執(zhí)行完畢以后返回一個(gè)Future

使用asyncawait還有一個(gè)好處是我們可以用和同步代碼相同的try/catch機(jī)制來做異常處理。

foo() async {
  try {
    print('foo E');
    var value = await bar();
    print('foo X $value');
  } catch (e) {
    // 同步執(zhí)行代碼中的異常和異步執(zhí)行代碼的異常都會(huì)被捕獲
  } finally {
    
  }
}

在日常使用場景中,我們通常利用asyncawait來異步處理IO,網(wǎng)絡(luò)請(qǐng)求,以及Flutter中的Platform channels通信等耗時(shí)操作。

總結(jié)

本文大致介紹了Flutter/Dart中的異步運(yùn)行機(jī)制,從異步運(yùn)行的基礎(chǔ)(Event Loop)開始,首先介紹了最原始的異步運(yùn)行機(jī)制,直接調(diào)度回調(diào)函數(shù);到Future;再到 asyncawait。了解了Flutter/Dart中的異步運(yùn)行機(jī)制是如何一步一步的進(jìn)化而來的。對(duì)于一直從事Native開發(fā),不太了解JavaScrip的同學(xué)來講,這個(gè)異步機(jī)制和原生開發(fā)有很大的不同,需要多多動(dòng)手練習(xí),動(dòng)腦思考才能適應(yīng)。本文中介紹的相關(guān)知識(shí)點(diǎn)較為粗淺,并沒有涉及dart:async中關(guān)于Future實(shí)現(xiàn)的源碼分析以及Stream等不太常用的類。這些如果大家想了解一下的話我會(huì)另寫文章來介紹一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容