在 Dart
中的非同步函式返回 Future
或 Stream
物件, await
和 async
關鍵字用於非同步程式設計, 使得編寫非同步程式碼就像同步程式碼一樣
使用 async
關鍵字標記一個函式為非同步函式, 如:
Future<String> fetchVersion() async {
return "1.0.0";
}
複製程式碼
獲取非同步函式 Future 的值
通過 await
關鍵獲取非同步函式 Future
的結果:
main() async {
var v = await fetchVersion();
print(v);
}
複製程式碼
除了通過 await
關鍵字獲取 Future
的值, 還可以通過 then
函式來獲取:
fetchVersion().then((version){
print(version);
});
複製程式碼
捕獲非同步函式異常
使用 try/catch/finally
來捕獲非同步函式 Future
的異常資訊:
try {
var v = await fetchVersion();
print(v);
} catch(e) {
print(e);
}
複製程式碼
如果是通過 then
函式來獲取的 Future
的結果, 可以通過 catchError
函式來捕獲異常:
fetchVersion().then((version) {
print(version);
}).catchError((e) {
print(e);
});
複製程式碼
非同步函式鏈
通過 then
函式來實現非同步函式的呼叫鏈:
Future result = costlyQuery(url);
result
.then((value) => expensiveWork(value))
.then((_) => lengthyComputation())
.then((_) => print('Done!'))
.catchError((exception) {
//Handle exception...
});
複製程式碼
也可以通過 await
關鍵字來實現上面的功能:
try {
final value = await costlyQuery(url);
await expensiveWork(value);
await lengthyComputation();
print('Done!');
} catch (e) {
//Handle exception...
}
複製程式碼
等待多個非同步Future
可以使用 Future.wait
函式來等待多個非同步函式返回, 該函式返回一個集合, 集合元素就是非同步函式的返回結果, 集合元素的順序就是非同步函式的順序:
main() async {
Future deleteLotsOfFiles() async {
return Future.delayed(Duration(seconds: 5), () {
return "deleteLotsOfFiles";
});
}
Future copyLotsOfFiles() async {
return ("copyLotsOfFiles");
};
Future checksumLotsOfOtherFiles() async {
return ("checksumLotsOfOtherFiles");
};
var result = await Future.wait([
deleteLotsOfFiles(),
copyLotsOfFiles(),
checksumLotsOfOtherFiles(),
]);
print(result);
print('Done with all the long steps!');
}
// 輸出結果
[deleteLotsOfFiles, copyLotsOfFiles, checksumLotsOfOtherFiles]
Done with all the long steps!
複製程式碼
await for
非同步函式返回 Future
, 也可以返回 Stream
, Stream
代表的是資料序列
可以通過 await for
來獲取 stream
裡的值, 如:
await for (varOrType identifier in stream) {
// Executes each time the stream emits a value.
}
複製程式碼
同理, 使用了 await for
的函式也必須是 async
:
Future main() async {
await for (var request in requestServer) {
handleRequest(request);
}
}
複製程式碼
在 await for
中可以使用 return
或 break
來中斷流
除了使用 await for
還可以使用 listen
函式來監聽 stream
裡的值:
main() {
new HttpClient().getUrl(Uri.parse('http://www.baidu.com'))
.then((HttpClientRequest request) => request.close())
.then((HttpClientResponse response) => response.transform(new Utf8Decoder()).listen(print));
}
複製程式碼
stream transform
有的時候我們還需要對流資料進行轉換, 例如下面的程式碼讀取檔案的內容案例, 對內容進行 utf8 的反編碼, 加上行分割, 這樣就能原樣輸出內容:
main() async {
var config = File('hello.dart');
var inputStream = config.openRead();
var lines = inputStream.transform(utf8.decoder).transform(LineSplitter());
await for (var line in lines) {
print(line);
}
}
複製程式碼
監聽流的異常和關閉
如何要捕獲上面讀取檔案的例子的異常, 如果是使用 await for
, 可以使用 try catch
main() async {
var config = File('config.txt');
Stream<List<int>> inputStream = config.openRead();
var lines = inputStream.transform(utf8.decoder).transform(LineSplitter());
try {
await for (var line in lines) {
print(line);
}
print('file is now closed');
} catch (e) {
print(e);
}
}
複製程式碼
如果使用的是 then
函式來實現的, 可以使用 onDone
和 onError
來監聽:
main() async {
var config = File('config.txt');
Stream<List<int>> inputStream = config.openRead();
inputStream.transform(utf8.decoder).transform(LineSplitter()).listen(
(String line) {
print(line);
}, onDone: () {
print('file is now closed');
}, onError: (e) {
print(e);
});
}
複製程式碼
create stream
上面的案例都是使用 SDK
為我們提供好的 Stream
, 那麼我們如何自己建立 Stream
呢?
新建一個返回 Stream
的非同步函式需要使用 async*
來標記, 使用 yield
或 yield*
來發射資料:
Stream<int> timedCounter(Duration interval, [int maxCount]) async* {
int i = 0;
while (true) {
await Future.delayed(interval);
yield i++;
if (i == maxCount) break;
}
}
main() async {
timedCounter(Duration(seconds: 2), 5).listen(print);
}
複製程式碼
上面的程式碼大概的意思就是每隔 interval = 2
秒發射一次資料, 資料從 0
開始累加, 直到資料等於 maxCount=5
時停止發射. 需要注意的是隻有呼叫了 listen
非同步函式體才會被執行, 該函式返回的是一個 Subscription
可以通過 pause
函式來暫停一個 Subscription
發射資料:
main() async {
var r = timedCounter(Duration(seconds: 2), 5).listen(print);
var resumeSignal = Future.delayed(Duration(seconds: 2));
r.pause(resumeSignal);
}
複製程式碼
執行了 pause
函式會導致 Subscription
暫停發射資料, resumeSignal
執行完畢後 Subscription
將繼續發射資料
pause
函式引數是可以選的, resumeSignal
引數可以不傳遞, 如:
main() async {
var r = timedCounter(Duration(seconds: 2), 5).listen(print);
r.pause();
}
複製程式碼
這個時候程式 2
秒後就會退出, 因為 執行完 listen
函式後,非同步函式體就會被執行, 當執行完
await Future.delayed(interval);
複製程式碼
程式便退·出了, 因為 main 函式已經執行完畢. 我們可以通過 resume
函式來恢復 Subscription
資料的發射:
main() async {
var r = timedCounter(Duration(seconds: 2), 5).listen(print);
r.pause();
await Future.delayed(Duration(seconds: 3));
r.resume();
}
複製程式碼
還可以通過 cancel
函式來取消一個 Subscription
:
main() async {
var r = timedCounter(Duration(seconds: 2), 5).listen(print);
//上面的程式碼意思4秒之後取消stream
Future.delayed(Duration(seconds: 4),(){
r.cancel();
});
}
複製程式碼
StreamController
有的時候 Stream
來程式的不同地方, 不僅僅是來自已非同步函式
我們可以通過 StreamController
來建立流, 並且往這個流裡發射資料. 也就是說 StreamController
會為我們建立一個新的 Stream
, 可以在任何時候任何地方為 Stream
新增 Event
例如我們使用 StreamController
來改造下 timedCounter
函式 :
Stream<int> timedCounter(Duration interval, [int maxCount]) {
var controller = StreamController<int>();
int counter = 0;
void tick(Timer timer) {
counter++;
// Ask stream to send counter values as event.
controller.add(counter);
if (maxCount != null && counter >= maxCount) {
timer.cancel();
// Ask stream to shut down and tell listeners.
controller.close();
}
}
// BAD: Starts before it has subscribers.
Timer.periodic(interval, tick);
return controller.stream;
}
複製程式碼
但是這個改造程式有兩個問題:
-
- 儘管沒有呼叫
listen
函式,timedCounter
函式體會被執行, 也就是定時器Timer.periodic()
會被執行, 因為timedCounter
並沒有被async*
標記, 如果被標記程式會自動幫我們處理.
- 儘管沒有呼叫
-
- 沒有處理
pause
情況, 哪怕執行了pause
函式, 程式也會不斷的產生事件
- 沒有處理
所以當我們使用 StreamController
的時候一定要避免出現上面的兩個問題, 否則會導致記憶體洩漏問題
對於第一個問題, 讀者可能會問:我們沒有執行 listen
函式, 程式也沒有輸出資料啊. 這是因為這些事件被 StreamController
緩衝起來了:
void listenAfterDelay() async {
var counterStream = timedCounter(const Duration(seconds: 1), 15);
await Future.delayed(const Duration(seconds: 5));
// After 5 seconds, add a listener.
await for (int n in counterStream) {
print(n); // Print an integer every second, 15 times.
}
}
複製程式碼
呼叫完 timedCounter
函式後, 我們沒有呼叫 listen
函式, 而是 delay
了 5
秒
也就是說 StreamController
在這 5
秒裡緩衝了 5
個 Event
然後通過 await for
來獲取值, 會立刻輸出 1 ~ 5
的數字, 因為這些資料已經緩衝好了, 等待被獲取.
最後每隔一秒輸出一次. 所以這個程式證明了哪怕沒有呼叫 listen
函式 傳送的 Event
會被 StreamController
緩衝起來
我們再來看下第二個問題: 沒有處理 pause
事件, 哪怕呼叫管理 pause
函式, 程式也會傳送 Event , 這些 Event
會被 StreamController
buffer 起來. 我們通過一個程式來驗證下:
void listenWithPause() {
var counterStream = timedCounter(const Duration(seconds: 1), 15);
StreamSubscription<int> subscription;
subscription = counterStream.listen((int counter) {
print(counter); // Print an integer every second.
if (counter == 5) {
// After 5 ticks, pause for five seconds, then resume.
subscription.pause(Future.delayed(const Duration(seconds: 5)));
}
});
}
複製程式碼
這個程式會輸出 1 ~ 5
的數, 然後會觸發 pause
函式, 暫停 5
秒. 其實在暫停的期間 Event
依然會傳送, 並沒有真正的暫停. 5
秒後 Subscription
恢復, 會立刻輸出 6 ~ 10
, 說明在這 5
秒產生了 5
個 Event
, 這些事件被 StreamController
buffer 起來了.
那麼在使用 StreamController
的時候如何避免上述問題呢?
Stream<int> timedCounter(Duration interval, [int maxCount]) {
StreamController<int> controller;
Timer timer;
int counter = 0;
void tick(_) {
counter++;
controller.add(counter); // Ask stream to send counter values as event.
if (counter == maxCount) {
timer.cancel();
controller.close(); // Ask stream to shut down and tell listeners.
}
}
void startTimer() {
timer = Timer.periodic(interval, tick);
}
void stopTimer() {
if (timer != null) {
timer.cancel();
timer = null;
}
}
controller = StreamController<int>(
onListen: startTimer,
onPause: stopTimer,
onResume: startTimer,
onCancel: stopTimer);
return controller.stream;
}
複製程式碼
也就是說需要把 pause/resume
等回撥交給 StreamController
, 它會自動幫我們處理好生命週期
關於 Dart
的非同步操作就介紹到這裡了.
下面是我的公眾號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯絡我: