Flutter (三) Dart 語言基礎詳解 (非同步,生成器,隔離,後設資料,註釋)

DevYK發表於2019-03-23

非同步

簡介

對於非同步不太瞭解,可以直接看官網介紹;

Dart 有一些語言特性來支援非同步程式設計。最常見的特性是 async 方法和 await 表示式。

Dart 庫中有很多返回 Future 或者 Stream 物件的方法。 這些方法是非同步的 : 這些函式在設定完基本的操作後就返回了 , 而無需等待操作執行完成。 例如讀取一個檔案 , 在開啟檔案後就返回了。

async 和 await

有兩種方式可以使用 Future 物件中的資料:

使用 asyncawait 的程式碼是非同步的, 但是看起來有點像同步程式碼。 例如,下面是一些使用 await 來 等待非同步方法返回的示例:

await lookUpVersion();
複製程式碼

要使用 await,其方法必須帶有 async 關鍵字:

checkVersion() saync {
   var version =  await lookUpVersion();
    if(version == expectedVersion){
        // TODO ------
    } else {
         // TODO ------
    }
} 
複製程式碼

可以使用 try, catch, 和 finally 來處理使用 await 的異常:

void getHttp(){
  try{
    var response = await Dio().get("http://www.baidu.com");
    print(response);
  }catch(e){
    print(e);
  }
}
複製程式碼

宣告非同步方法

  • 一個 async 方法 是函式體被標記為 async 的方法。 雖然非同步方法的執行可能需要一定時間,但是 非同步方法立刻返回 - 在方法體還沒執行之前就返回了。

    void getHttp async {
        // TODO ---
    }
    複製程式碼

在一個方法上新增 async 關鍵字,則這個方法返回值為 Future。 例如,下面是一個返回字串的同步方法:

String loadAppVersion() => "1.0.2"
複製程式碼

如果使用 async 關鍵字,則該方法返回一個 Future,並且 認為該函式是一個耗時的操作。

Futre<String> loadAppVersion() async  => "1.0.2"
複製程式碼

注意,方法的函式體並不需要使用 Future API。 Dart 會自動在需要的時候建立 Future 物件。

推薦使用 async / await 而不是直接使用底層的特性

  • 好的習慣
void main() {
 //呼叫非同步方法
 doAsync();
}

// 在函式上宣告瞭 async 表明這是一個非同步方法
Future<bool> doAsync() async {
  try {
    // 這裡是一個模擬請求一個網路耗時操作
    var result = await getHttp();
    //請求出來的結果
    return printResult(result);
  } catch (e) {
    print(e);
    return false;
  }
}
//將請求出來的結果列印出來
Future<bool> printResult(summary) {
  print(summary);
}

//開始模擬網路請求 等待 5 秒返回一個字串
getHttp() {
 return new Future.delayed(Duration(seconds: 5), () => "Request Succeeded");
}
複製程式碼
  • 壞的習慣寫法
void main() {
 doAsync();
}

Future<String> doAsync() async {
    return  getHttp().then((r){
      return printResult(r);
    }).catchError((e){
      print(e);
    });
}

Future<String> printResult(summary) {
  print(summary);
}

Future<String> getHttp() {
 return new Future.delayed(Duration(seconds: 5), () => "Request Succeeded");
}
複製程式碼

then,catchError,whenComplete

先來看一段程式碼

void doAsyncs() async{
  //then catchError whenComplete
  new Future(() => futureTask()) //  非同步任務的函式
      .then((m) => "1-:$m") //   任務執行完後的子任務
      .then((m) => print('2-$m')) //  其中m為上個任務執行完後的返回的結果
      .then((_) => new Future.error('3-:error'))
      .then((m) => print('4-'))
      .whenComplete(() => print('5-')) //不是最後執行whenComplete,通常放到最後回撥

//      .catchError((e) => print(e))//如果不寫test預設實現一個返回true的test方法
      .catchError((e) => print('6-catchError:' + e), test: (Object o) {
    print('7-:' + o);
    return true; //返回true,會被catchError捕獲
//        return false; //返回false,繼續丟擲錯誤,會被下一個catchError捕獲
  })
  .then((_) => new Future.error('11-:error'))
      .then((m) => print('10-'))
      .catchError((e) => print('8-:' + e))
      ;
}

 futureTask() {
//  throw 'error';
  return Future.delayed(Duration(seconds: 5),()  => "9-走去跑步");
}
複製程式碼

大家猜一猜,看下是怎麼樣的一個執行流程;我直接放上答案吧,我們來分析下

2-1-:9-走去跑步
5-
7-:3-:error
6-catchError:3-:error
8-:11-:error
複製程式碼

當非同步函式 futureTask() 執行完會在記憶體中儲存 ‘9-走去跑步’ 然後繼續執行下一步 這個時候遇見了 then 現在會在記憶體中儲存 “1-: 9-走去跑步 ” 繼續執行 這個時候遇見了列印輸出 2-1-:9-走去跑步 。現在第一個列印出來了。接著執行下一個 then() 這個時候遇見了一個 error 異常,Dart 會把這個異常儲存在記憶體直到遇見捕獲異常的地方。下面執行 whenComplete 這個函式 列印 5- 。然後遇見了一個捕獲異常的函式 catchError 如果 test 返回 true ,會被 catchError 捕獲 列印 7-:3-:error 6-catchError:3-:error。如果返回 false 只列印 7-:3-:error,會把 error 拋給下一個 catchError 。繼續執行 又遇見了一個 error 11-:error ,現在出現 error 了 所以 then 10- 就不會執行了 。最後就直接捕獲異常 列印 "8-11-error"。

分析完了,大家會了嗎?相信大家差不多會了!

Event-Looper

以下內容從官網所得到

Dart 是單執行緒模型 Main, 也就沒有了所謂的主執行緒/子執行緒之分。

Dart 也是 Event-Loop 以及 Event - Queue 的模型,所有的事件都是通過 Event-loop 的依次執行。

而 Dart 的 Event Loop 就是:

  • 從 EventQueue 中獲取 Event

  • 處理 Event

  • 直到 EventQueue 為空

    1553334610.jpg

而這些 Event 包括了使用者輸入,點選, Timer, 檔案 IO 等

1553334796.jpg

單執行緒模型

一旦某個 Dart 的函式開始執行,它將執行到這個函式結束,也就是 Dart 的函式不會被其他 Dart 程式碼打斷。

Dart 中沒有執行緒的概念,只有 isolate(隔離),每個 isolate 都是隔離的,並不會共享記憶體。而一個 Dart 程式是在 Main isolate 的 main 函式開始,而在 Main 函式結束後,Main isolate 執行緒開始一個一個(one by one)的開始處理 Event Queue 中的每一個 Event 。

asgrargargaw.webp

Event Queue 和 Microtask Queue

Dart 中的 Main Isolate 只有一個 Event - Looper,但是存在兩個 Event Queue : Event Queue 以及 Microtask Queue Microtask Queue 存在的意義是: 希望通過這個 Queue 來處理稍晚一些的事情,但是在下一個訊息到來之前需要處理完的事情。 當 Event Looper 正在處理 Microtask Queue 中的 Event 時候,Event Queue 中的 Event 就停止了處理了,此時 App 不能繪製任何圖形,不能處理任何滑鼠點選,不能處理檔案 IO 等等。

Event-Looper 挑選 Task 的執行順序為:

  • 優先全部執行完 Microtask Queue.

  • 直到 Microtask Queue 為空時,才會執行 Event Queue 中的 Event.

    1941624-f7acc83a0816453f.png

Dart 中只能知道 Event 處理的先後順序,但是並不知道某個 Event 執行的具體時間點,因為它的處理模型是一個單執行緒迴圈,而不是基於時鐘排程(即它的執行只是按照 Event 處理完,就開始迴圈下一個 Event,而與Java 中的 Thread 排程不一樣,沒有時間排程的概念),也就是我們既是指定另一個 Delay Time 的 Task,希望它在預期的時間後開始執行,它有可能不會在那個時間執行,需要看是否前面的 Event 是否已經 Dequeue 。

任務排程

當有程式碼可以在後續任務執行的時候,有兩種方式,通過dart:async這個Lib中的API即可:

  • 使用 Future 類,可以將任務加入到 Event Queue 的隊尾
  • 使用 scheduleMicrotask 函式,將任務加入到 Microtask Queue 隊尾

當使用 EventQueue 時,需要考慮清楚,儘量避免 microtask queue 過於龐大,否則會阻塞其他事件的處理

1941624c2926ff39b5a2ac9.webp

new Future()

先來看一段程式碼

void testFuture() {
  Future f = new Future(() => print("1"));
  Future f1 = new Future(() => null);
  Future f2 = new Future(() => null);
  Future f3 = new Future(() => null);
    
  f3.then((_) => print("2"));
  f2.then((_) {
    print("3");
    new Future(() => print("4"));
    f1.then((_) {
      print("5");
    });
  });
  f1.then((m) {
    print("6");
  });
  print("7");
}
複製程式碼

大家來猜一猜上面的列印效果,列印結果是:

7
1
6
3
5
2
4
複製程式碼

是不是沒有想到。下面我們來分析下執行流程

  1. main 函式執行完的時候,f, f1, f2, f3 會進入事件佇列,這裡 print( 7 )不進入佇列 直接列印 7;
  2. 佇列裡面又執行 f 事件,發現 f 存在一個列印事件 print(1) 直接列印 1;
  3. 現在又執行 f1 事件,發現了一個 f1.then(m) => print('6') 直接列印 6;
  4. 執行到了 f2 事件,發現了 f2 事件裡面有一個 列印事件 3,先列印一個 3
    1. 然後遇見了一個新的時間 f4,放入新的事件佇列裡面。
    2. 現在又發現了一個 f1 的事件 print('5'),這裡的 f1 因為執行完了 返回的 null 所以要把該事件存放入 微佇列 裡面。微佇列裡面優先執行所以列印 5 ;
  5. 現在該執行到了 f2 事件了,發現了一個 print(2), 直接列印 2;
  6. 最後執行到了 f4 事件,發現了一個 print(4),直接列印 4;

注意:這裡的 f,f1,f2,f3... 進入事件佇列裡面要把它們理解成是一個整體,不管其它地方 還有 f1.then 或者 f2.then 。最後都是一個整體。

注意

  1. 使用 new Future 將任務加入 event 佇列。

  2. Future 中的 then 並沒有建立新的 Event 丟到 Event Queue 中,而只是一個普通的 Function Call ,

    FutureTask 執行完後,立即開始執行。

  3. 如果在 then() 呼叫之前 Future 就已經執行完畢了,那麼任務會被加入到 microtask 佇列中,並且該任務會執行 then() 中註冊的回撥函式。

  4. 使用 Future.value 建構函式的時候,就會上一條一樣,建立 Task 丟到 microtask Queue 中執行 then 傳入的函式。

  5. Future.sync 建構函式執行了它傳入的函式之後,也會立即建立 Task 丟到 microtask Queue 中執行。

  6. 當任務需要延遲執行時,可以使用 new Future.delay() 來將任務延遲執行。

scheduleMicrotask();

看一段程式碼,然後再來分析它們的執行流程

//scheduleMicrotask
void testScheduleMicrotask() {
  //918346572
  scheduleMicrotask(() => print('s1'));

  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('s10'))
      .then((_) => new Future(() => print('s11')))
      .then((_) => print('s12'));

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

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

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

列印結果

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

這一次大家自己分析了 咳咳 ~ 。

注意

  1. 如果可以,儘量將任務放入event 佇列中。
  2. 使用 Future 的 then 方法或 whenComplete 方法來指定任務順序。
  3. 為了保持你 app 的可響應性,儘量不要將大計算量的任務放入這兩個佇列。
  4. 大計算量的任務放入額外的 isolate 中。

生成器

同步生成器

//同步生成器
//呼叫getSyncGenerator 立刻返回 Iterable
void main() {
  var it = getSyncGenerator(5).iterator;
  //  呼叫moveNext方法時getSyncGenerator才開始執行
  while (it.moveNext()) {
    print(it.current);
  }
}

//同步生成器: 使用sync*,返回的是Iterable物件
Iterable<int> getSyncGenerator(int n) sync* {
  print('start');
  int k = n;
  while (k > 0) {
    //yield會返回moveNext為true,並等待 moveNext 指令
    yield k--;
  }
  print('end');
}
複製程式碼

注意

  1. 使用 sync* ,返回的是 Iterable 物件。
  2. yield 會返回 moveNext 為 true ,並等待 moveNext 指令。
  3. 呼叫 getSyncGenerator 立即返回 Iterable 物件。
  4. 呼叫 moveNext 方法時 getSyncGenerator 才開始執行。

非同步生成器

//非同步生成器呼叫
// getAsyncGenerator立即返回Stream,只有執行了listen,函式才會開始執行
  StreamSubscription subscription = getAsyncGenerator(5).listen(null);
  subscription.onData((value) {
    print(value);
    if (value >= 2) {
      subscription.pause(); //可以使用StreamSubscription物件對資料流進行控制
    }
  });

//非同步生成器: 使用async*,返回的是Stream物件
Stream<int> getAsyncGenerator(int n) async* {
  print('start');
  int k = 0;
  while (k < n) {
    //yield不用暫停,資料以流的方式一次性推送,通過StreamSubscription進行控制
    yield k++;
  }
  print('end');
}
複製程式碼

注意

  1. 使用 async* ,返回的是 Stream 物件。
  2. yield 不用暫停,資料以流的方式一次性推送,通過 StreamSubscription 進行控制。
  3. 呼叫 getAsyncGenerator 立即返回 Stream ,只有執行了 listen ,函式才會開始執行。
  4. listen 返回一個 StreamSubscription 物件進行流監聽控制。
  5. 可以使用 StreamSubscription 物件對資料流進行控制。

遞迴生成器

非同步

void main(){
      //非同步
  getAsyncRecursiveGenerator(5).listen((value) => print(value));
}

//非同步遞迴生成器
Stream<int> getAsyncRecursiveGenerator(int n) async* {
  if (n > 0) {
    yield n;
    yield* getAsyncRecursiveGenerator(n - 1);
  }
}

複製程式碼

同步

void main (){
  //遞迴生成器
  //同步
  var it1 = getSyncRecursiveGenerator(5).iterator;
  while (it1.moveNext()) {
    print(it1.current);
  }
}

//遞迴生成器:使用yield*
Iterable<int> getSyncRecursiveGenerator(int n) sync* {
  if (n > 0) {
    yield n;
    yield* getSyncRecursiveGenerator(n - 1);
  }
}
複製程式碼

注意

  1. yield* 以指標的方式傳遞遞迴物件,而不是整個同步物件。

隔離

Isolates

現代的瀏覽器以及移動瀏覽器都執行在多核 CPU 系統上。 要充分利用這些 CPU,開發者一般使用共享記憶體 資料來保證多執行緒的正確執行。然而, 多執行緒共享資料通常會導致很多潛在的問題,並導致程式碼執行出錯。

所有的 Dart 程式碼在 isolates 中執行而不是執行緒。 每個 isolate 都有自己的堆記憶體,並且確保每個 isolate 的狀態都不能被其他 isolate 訪問。

後設資料

(註解)-@deprecated

main() {
  dynamic tv = new Television();
  tv.activate();
  tv.turnOn();
}

class Television {
  @deprecated
  void activate() {
    turnOn();
  }

  void turnOn() {
    print('Television turn on!');
  }
}

複製程式碼

(註解)-@override

main() {
  dynamic tv = new Television();
  tv.activate();
  tv.turnOn();
  tv.turnOff();
}

class Television {
  @deprecated
  void activate() {
    turnOn();
  }
  
  void turnOn() {
    print('Television turn on!');
  }
  @override
  noSuchMethod(Invocation mirror) {
    print('沒有找到方法');
  }
}

複製程式碼

注意

  1. 所有的 Dart 程式碼都可以使用: @deprecated 和 @override。

(註解)-自定義

建立 todo.dart 檔案

//todo.dart

class Todo {
  final String who;
  final String what;

  const Todo({this.who, this.what});
}

複製程式碼
import 'todo.dart’;

main() {
  dynamic tv = new Television();
  tv.doSomething();
}

class Television {
  @Todo(who: 'damon', what: 'create a new method')
  void doSomething() {
    print('doSomething');
  }
}

複製程式碼

注意

  1. 在 java 中,如果自定義一個註解,需要新增 @Target 作用域註解,@Retention 註解型別註解,新增 @interface,然後定義註解引數。
  2. 構造方法定義為編譯時常量

註釋

單行註釋

// 跟 Java 一樣
複製程式碼

多行註釋

/**跟 Java 一樣**/
複製程式碼

文件註釋

/// 跟 Java 一樣
複製程式碼

總結

三篇基礎 內容介紹了常見的 Dart 語言特性。 還有更多特性有待實現,但是新的特性不會破壞已有的程式碼。 更多資訊請參考 Dart 語言規範Effective Dart

要了解 Dart 核心庫的詳情,請參考 Dart 核心庫預覽

相關文章