那些你不知道的Dart細節之帶你透徹理解非同步

Zhujiang發表於2020-04-10

那些你不知道的Dart細節之帶你透徹理解非同步

前言

上週一口氣寫了五篇Dart基礎文章,建議看這篇文章之前先看一下前幾篇文章:

那些你不知道的Dart細節之變數

那些你不知道的Dart細節之內建型別

那些你不知道的Dart細節之函式(方法)

那些你不知道的Dart細節之操作符、流程控制語句、異常

那些你不知道的Dart細節之類的點點滴滴

那些你不知道的Dart細節之泛型和庫

好了,廢話不多說,開始進入今天的主題吧!

async和await

開始說這兩個關鍵字之前我覺得有必要提一下:在Dart中沒有子執行緒一說,所有程式碼都是在一條主線上執行的,所以需要用非同步來實現一些耗時操作。(如果非要開啟多執行緒需要使用隔離,這裡不做敘述)

來說一下這兩個關鍵字吧,async用來修飾方法,需要寫在方法括號的後面,await寫在方法裡面,這裡要注意:await關鍵字必須在async函式內部使用,不然會報錯。await表示式可以使用多次。,這裡其實很好理解:都不是非同步方法了你還等待啥啊?下面看一個簡單的樣例吧:

void main() {
  getName1();
  getName2();
  getName3();
}

getName1() async {
  await getStr1();
  await getStr2();
  print('getName1');
}

getStr1() {
  print('getStr1');
}

getStr2() {
  print('getStr2');
}

getName2() {
  print('getName2');
}

getName3() {
  print('getName3');
}

複製程式碼

上面這段程式碼並不長,大家可以猜一下列印出來的值。

我們們來一步一步分析吧,後面再貼出來答案,看看大家猜的對不對。

首先執行getName1(),執行的時候發現這個方法是async的方法,繼續執行,執行到方法中第一行的時候,發現呼叫了一個getStr1()方法,而且這個方法使用了await來修飾,表示需要等待執行,重點來了:當遇到await的時候會執行完這一行,列印出了getStr1,之後立即返回一個Future(void)物件(上面的程式碼中省略了這個,寫程式碼時推薦加上,方便程式碼閱讀理解),然後將這個方法中剩餘的程式碼放入了事件佇列,接著往下執行getName2()和getName3(),分別列印出了getName2和getName3,剛才也說過,在Dart中只有一個main執行緒一桶到底,還有一個事件佇列,現在main執行緒中都已經執行完畢,但是事件佇列中還有東西,繼續執行getStr2(),執行的時候發現還是await,再進行等待,等待執行完成後列印getStr2,最後再列印getName1

下面是列印出的結果:

getStr1
getName2
getName3
getStr2
getName1
複製程式碼

和大家猜的一樣嗎?為什麼會這樣列印上面已經進行了分析。下面我們們看一下其他的幾個關鍵字。

then,catchError,whenComplete

上面分析中說過,async方法中遇到await時即會返回一個Future物件,從字面上也能知道這個一個未來的值,那麼肯定需要等待完成之後才能獲取到裡面的值。then關鍵字的意思就是獲取等待執行完畢之後返回的值,光說感覺說不明白,還是來看一段程式碼吧:

void main() {
  new Future(() => futureTask())//非同步任務的函式
      .then((i) => "result:$i")//任務執行完後的子任務
      .then((m) => print(m)); //其中m為上個任務執行完後的返回的結果
}

futureTask() {
  return 10;
}
複製程式碼

上面這段程式碼中只有一個列印,下面是列印出的值:

result:10
複製程式碼

為什麼不只顯示10呢?因為第二次then的時候引數m是第一次then返回的值,而不是futureTask()返回的10這裡應該不難理解。

在then之後還可以拋異常,下面來拋一個異常來看看:

new Future(() => futureTask())//非同步任務的函式
      .then((i) => "result:$i")//任務執行完後的子任務
      .then((m) => print(m)) //其中m為上個任務執行完後的返回的結果
      .then((_) => Future.error("出錯了"));
複製程式碼

從上面程式碼中可以知道拋異常的方法Future.error("出錯了"),來看一下執行情況:

result:10
Unhandled exception:
出錯了
#0      _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1114:29)
#1      _microtaskLoop (dart:async/schedule_microtask.dart:43:21)
#2      _startMicrotaskLoop (dart:async/schedule_microtask.dart:52:5)
#3      _Timer._runTimers (dart:isolate-patch/timer_impl.dart:393:30)
#4      _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:418:5)
#5      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:174:12)
複製程式碼

既然可以拋異常了,當然也可以catch異常,直接使用catchError關鍵字來捕獲一下:

new Future(() => futureTask())//非同步任務的函式
      .then((i) => "result:$i")//任務執行完後的子任務
      .then((m) => print(m)) //其中m為上個任務執行完後的返回的結果
      .then((_) => Future.error("出錯了"))
      .catchError(print);
複製程式碼

寫法很簡單,直接參照上面程式碼寫即可,下面是執行結果:

result:10
出錯了
複製程式碼

?,已經捕獲了一場,需要做什麼事的可以在裡面進行操作了。

對了,標題中還有一個關鍵字沒說,whenComplete,這個關鍵字的意思簡單,指所有任務完成後的回撥函式,使用也很簡單,直接看程式碼:

new Future(() => futureTask())//非同步任務的函式
      .then((i) => "result:$i")//任務執行完後的子任務
      .then((m) => print(m)) //其中m為上個任務執行完後的返回的結果
      .then((_) => Future.error("出錯了"))
      .catchError(print)
      .whenComplete(() => print("whenComplete"));//所有任務完成後的回撥函式
複製程式碼

看一下執行結果:

result:10
出錯了
whenComplete
複製程式碼

很簡單對吧?下面說的就有點噁心了。。。?

Event-Looper

那些你不知道的Dart細節之帶你透徹理解非同步

那些你不知道的Dart細節之帶你透徹理解非同步

上面兩張圖說的就是Dart的Event-Looper。其實也不難理解:

  • 一個訊息迴圈的職責就是不斷從訊息佇列中取出訊息並處理他們直到訊息佇列為空。
  • 訊息佇列中的訊息可能來自使用者輸入,檔案I/O訊息,定時器等。例如上圖的訊息佇列就包含了定時器訊息和使用者輸入訊息。
  • Dart中的Main Isolate只有一個Event Looper,但是存在兩個Event Queue: Event Queue以及Microtask Queue。

簡單瞭解一下就行,只要記著它就是一個訊息佇列,如果有值就一直迴圈取出進行處理就夠了。第三點說的事件佇列和微佇列在下面說吧,這裡沒有例子不太好理解。

new Future()

到這裡本文的核心知識點就來了,首先來一張我自己的手繪圖吧:

那些你不知道的Dart細節之帶你透徹理解非同步

嗯。。。不要在意畫的,看重點:Dart中方法執行無外乎這三個地方,主執行緒main,事件佇列(就是上面說的 Event Queue)和微佇列(上面的Microtask Queue),執行順序不太一樣樣,先執行main執行緒,然後是微佇列,最後是事件佇列。這裡有一個坑,我們們從下面的程式碼中來徹底理解一下:

void main(){
  testFuture();
}
void testFuture() {
  Future f = new Future(() => print('f1'));
  Future f1 = new Future(() => null);
  Future f2 = new Future(() => null);
  Future f3 = new Future(() => null);
  f3.then((_) => print('f2'));
  f2.then((_) {
    print('f3');
    new Future(() => print('f4'));
    f1.then((_) {
      print('f5');
    });
  });
  f1.then((m) {
    print('f6');
  });
  print('f7');
}
複製程式碼

還是和上面一樣,大家來猜一下執行結果,我先來分析,最後我會把執行結果貼出來,不想看分析的可以直接看執行結果檢視一下自己的判斷是否正確。

首先,main中執行了方法testFuture(),在testFuture()方法中在main執行緒的只有print('f7'),其他的都在事件佇列,所以先列印了f7***,main執行緒已經執行完畢,再來看事件佇列,執行事件佇列之前需要檢視一下微佇列中是否有東西,如果微佇列中有東西的話需要先執行*(這就是上面所說的坑),來看一下現在的佇列中都有啥吧:

那些你不知道的Dart細節之帶你透徹理解非同步

事件佇列中現在依次放著f、f1、f2和f3,f直接列印了,ok,這裡列印出了f1f1、f2和f3還在事件佇列中,這裡有一些迷惑,程式碼中我先放的是f3的then,但是執行的時候並不是先執行f3的then,而是根據事件佇列中的順序來執行的,事件佇列中的f已經執行完畢,接下來該執行f1,f1列印出了f6,f1也執行完畢,該執行f2,f2的then中東西就比較多了,這裡也有今天的重中之重。執行f2的then,先列印出了f3,然後又新增了一個Future,記著,這裡不是直接執行,而是將新增的f4也放入了事件佇列中,再往下執行,用到了f1的then,但是f1我們們剛才已經執行完畢了,這裡又對f1進行了呼叫,那麼這個時候就需要把f1放入微佇列中,再來看一下現在的佇列中都有啥:

那些你不知道的Dart細節之帶你透徹理解非同步

還記得剛才說的嗎?本來f2我們們已經執行完成該執行f3了,但是現在微佇列中有了東西,我們們就需要先執行微佇列中的東西,OK,又列印出了f5,接著執行事件佇列中的f3,列印出了f2,最後執行f4,列印出了f4

到這裡就基本分析完了,看完分析的應該已經知道列印的值了,來看一下吧:

f7
f1
f6
f3
f5
f2
f4
複製程式碼

和我們們分析的一樣,這裡需要注意的就是微佇列和事件佇列的關係

scheduleMicrotask()

上面程式碼中f1進入了微佇列是因為執行完畢之後再執行,故而放入了微佇列中,這是被動進去的,就不能主動放入微佇列中嗎?當然可以,使用scheduleMicrotask()就可以放入微佇列中。

經過上面的一段程式碼相信大家對非同步已經有了一定了解,那麼下面再來一段稍微複雜點的加上了scheduleMicrotask()的程式碼:

void main(){
  testScheduleMicrotask();
}
void testScheduleMicrotask(){
  scheduleMicrotask(() => print('s1'));//微任務
  //delay 延遲
  new Future.delayed(new Duration(seconds: 1), () => print('s2'));

  new Future(() => print('s3')).then((_) {
    print('s4');
    scheduleMicrotask(() => print('s5'));
  }).then((_) => print('s6'));

  new Future(() => print('s7'));

  scheduleMicrotask(() => print('s8'));

  print('s9');
}
複製程式碼

還是和上面一樣,還是同樣的mian執行緒、事件佇列和微佇列,還是先來分析程式碼,最後再貼出執行結果。

上面程式碼中main執行緒中只有一個print('s9'),那麼就先列印s9,然後有兩個主動建立的微佇列,s2進行了延遲,這裡注意,進行了延遲直接放入佇列的末尾,所以說最後一個列印的是s2。然後s3放入了事件佇列的第一個,s7放入了第二個,再來看一下現在的佇列吧:

那些你不知道的Dart細節之帶你透徹理解非同步

在執行事件佇列前先看一下微佇列中是否有東西,ok,微佇列中有,先來執行微佇列,分別列印出了s1和s8,再來執行事件佇列s3,先列印出了s3,再列印出了s4,又出現了微佇列s5,這裡注意:先將s5放入微佇列中,並不是直接執行,而是執行完s3的事件佇列之後,在執行s7之前再去檢視微佇列是否有值,來看一下現在的佇列樣式:

那些你不知道的Dart細節之帶你透徹理解非同步

所以這裡又列印了s6,再列印了微佇列中的s5,之後執行事件佇列中的s7,列印出了s7,最後再列印出延時操作的s2

好了,分析完畢,執行結果已經出來了,來看一下吧:

s9
s1
s8
s3
s4
s6
s5
s7
s2
複製程式碼

這裡有幾點需要大家注意:

  • 如果可以,儘量將任務放入event佇列中
  • 使用Future的then方法或whenComplete方法來指定任務順序
  • 為了保持你app的可響應性,儘量不要將大計算量的任務放入這兩個佇列
  • 大計算量的任務放入額外的isolate中(isolate就是上面提到了隔離,這裡不做解釋。)

總結

好了,到這裡本篇文章就到尾聲了。本篇文章帶大家一起看了一下Dart中的非同步執行順序,只要理解了上面的兩端程式碼,其實Dart的非同步你已經基本掌握了,剩下的只有多寫程式碼去練、去理解才行。這是我Dart的第七篇文章了,也是Dart基礎的最後一篇文章,前六篇大家可以通過文章開頭的連結去訪問,如果文章對你有幫助,別忘記點贊關注;如果對文章有異議,可以在下面評論區指出來,共同學習進步,不勝感激。

相關文章