非同步
簡介
Dart 有一些語言特性來支援非同步程式設計。最常見的特性是 async
方法和 await
表示式。
Dart 庫中有很多返回 Future 或者 Stream 物件的方法。 這些方法是非同步的 : 這些函式在設定完基本的操作後就返回了 , 而無需等待操作執行完成。 例如讀取一個檔案 , 在開啟檔案後就返回了。
async 和 await
有兩種方式可以使用 Future 物件中的資料:
- 使用 async 和 await
- 使用 Future API
使用 async
和 await
的程式碼是非同步的, 但是看起來有點像同步程式碼。 例如,下面是一些使用 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 為空
而這些 Event 包括了使用者輸入,點選, Timer, 檔案 IO 等
單執行緒模型
一旦某個 Dart 的函式開始執行,它將執行到這個函式結束,也就是 Dart 的函式不會被其他 Dart 程式碼打斷。
Dart 中沒有執行緒的概念,只有 isolate(隔離),每個 isolate 都是隔離的,並不會共享記憶體。而一個 Dart 程式是在 Main isolate 的 main 函式開始,而在 Main 函式結束後,Main isolate 執行緒開始一個一個(one by one)的開始處理 Event Queue 中的每一個 Event 。
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.
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 過於龐大,否則會阻塞其他事件的處理
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
複製程式碼
是不是沒有想到。下面我們來分析下執行流程
- main 函式執行完的時候,f, f1, f2, f3 會進入事件佇列,這裡 print( 7 )不進入佇列 直接列印 7;
- 佇列裡面又執行 f 事件,發現 f 存在一個列印事件 print(1) 直接列印 1;
- 現在又執行 f1 事件,發現了一個 f1.then(m) => print('6') 直接列印 6;
- 執行到了 f2 事件,發現了 f2 事件裡面有一個 列印事件 3,先列印一個 3
- 然後遇見了一個新的時間 f4,放入新的事件佇列裡面。
- 現在又發現了一個 f1 的事件 print('5'),這裡的 f1 因為執行完了 返回的 null 所以要把該事件存放入 微佇列 裡面。微佇列裡面優先執行所以列印 5 ;
- 現在該執行到了 f2 事件了,發現了一個 print(2), 直接列印 2;
- 最後執行到了 f4 事件,發現了一個 print(4),直接列印 4;
注意:這裡的 f,f1,f2,f3... 進入事件佇列裡面要把它們理解成是一個整體,不管其它地方 還有 f1.then 或者 f2.then 。最後都是一個整體。
注意
-
使用 new Future 將任務加入 event 佇列。
-
Future 中的 then 並沒有建立新的 Event 丟到 Event Queue 中,而只是一個普通的 Function Call ,
FutureTask 執行完後,立即開始執行。
-
如果在 then() 呼叫之前 Future 就已經執行完畢了,那麼任務會被加入到 microtask 佇列中,並且該任務會執行 then() 中註冊的回撥函式。
-
使用 Future.value 建構函式的時候,就會上一條一樣,建立 Task 丟到 microtask Queue 中執行 then 傳入的函式。
-
Future.sync 建構函式執行了它傳入的函式之後,也會立即建立 Task 丟到 microtask Queue 中執行。
-
當任務需要延遲執行時,可以使用 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
複製程式碼
這一次大家自己分析了 咳咳 ~ 。
注意
- 如果可以,儘量將任務放入event 佇列中。
- 使用 Future 的 then 方法或 whenComplete 方法來指定任務順序。
- 為了保持你 app 的可響應性,儘量不要將大計算量的任務放入這兩個佇列。
- 大計算量的任務放入額外的 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');
}
複製程式碼
注意
- 使用 sync* ,返回的是 Iterable 物件。
- yield 會返回 moveNext 為 true ,並等待 moveNext 指令。
- 呼叫 getSyncGenerator 立即返回 Iterable 物件。
- 呼叫 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');
}
複製程式碼
注意
- 使用 async* ,返回的是 Stream 物件。
- yield 不用暫停,資料以流的方式一次性推送,通過 StreamSubscription 進行控制。
- 呼叫 getAsyncGenerator 立即返回 Stream ,只有執行了 listen ,函式才會開始執行。
- listen 返回一個 StreamSubscription 物件進行流監聽控制。
- 可以使用 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);
}
}
複製程式碼
注意
- 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('沒有找到方法');
}
}
複製程式碼
注意
- 所有的 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');
}
}
複製程式碼
注意
- 在 java 中,如果自定義一個註解,需要新增 @Target 作用域註解,@Retention 註解型別註解,新增 @interface,然後定義註解引數。
- 構造方法定義為編譯時常量
註釋
單行註釋
// 跟 Java 一樣
複製程式碼
多行註釋
/**跟 Java 一樣**/
複製程式碼
文件註釋
/// 跟 Java 一樣
複製程式碼
總結
這 三篇基礎 內容介紹了常見的 Dart 語言特性。 還有更多特性有待實現,但是新的特性不會破壞已有的程式碼。 更多資訊請參考 Dart 語言規範 和 Effective Dart。
要了解 Dart 核心庫的詳情,請參考 Dart 核心庫預覽。