文章目錄
異步之 Stream 詳解
關於Dart 語言的Stream 部分,應該回到語言本身去尋找答案,許多資料在Flutter框架中囫圇吞棗式的解釋Stream
,總有一種讓人雲山霧罩的感覺,事實上從Dart語言本身去了解Stream並不複雜,接下來就花點時間好好學習一下Stream
吧!
Stream
和 Future
都是Dart中異步編程的核心內容,在之前的文章中已經詳細敘述了關於Future
的知識,請查看 Dart 異步編程詳解,本篇文章則主要基於 Dart2.5 介紹Stream
的知識。
什麼是Stream
Stream
是Dart語言中的所謂異步數據序列的東西,簡單理解,其實就是一個異步數據隊列而已。我們知道隊列的特點是先進先出的,Stream
也正是如此
更形象的比喻,Stream
就像一個傳送帶。可以將一側的物品自動運送到另一側。如上圖,在另一側,如果沒有人去抓取,物品就會掉落消失。
但如果我們在末尾設置一個監聽,當物品到達末端時,就可以觸發相應的響應行爲。
在Dart語言中,
Stream
有兩種類型,一種是點對點的單訂閱流(Single-subscription),另一種則是廣播流。
單訂閱流
單訂閱流的特點是隻允許存在一個監聽器,即使該監聽器被取消後,也不允許再次註冊監聽器。
創建 Stream
創建一個Stream
有9個構造方法,其中一個是構造廣播流的,這裏主要看一下其中5個構造單訂閱流的方法
periodic
void main(){
test();
}
test() async{
// 使用 periodic 創建流,第一個參數爲間隔時間,第二個參數爲回調函數
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
// await for循環從流中讀取
await for(var i in stream){
print(i);
}
}
// 可以在回調函數中對值進行處理,這裏直接返回了
int callback(int value){
return value;
}
打印結果:
0
1
2
3
4
...
該方法從整數0開始,在指定的間隔時間內生成一個自然數列,以上設置爲每一秒生成一次,callback
函數用於對生成的整數進行處理,處理後再放入Stream
中。這裏並未處理,直接返回了。要注意,這個流是無限的,它沒有任何一個約束條件使之停止。在後面會介紹如何給流設置條件。
fromFuture
void main(){
test();
}
test() async{
print("test start");
Future<String> fut = Future((){
return "async task";
});
// 從Future創建Stream
Stream<String> stream = Stream<String>.fromFuture(fut);
await for(var s in stream){
print(s);
}
print("test end");
}
打印結果:
test start
async task
test end
該方法從一個Future
創建Stream
,當Future
執行完成時,就會放入Stream
中,而後從Stream
中將任務完成的結果取出。這種用法,很像異步任務隊列。
fromFutures
從多個Future
創建Stream
,即將一系列的異步任務放入Stream
中,每個Future
按順序執行,執行完成後放入Stream
import 'dart:io';
void main() {
test();
}
test() async{
print("test start");
Future<String> fut1 = Future((){
// 模擬耗時5秒
sleep(Duration(seconds:5));
return "async task1";
});
Future<String> fut2 = Future((){
return "async task2";
});
// 將多個Future放入一個列表中,將該列表傳入
Stream<String> stream = Stream<String>.fromFutures([fut1,fut2]);
await for(var s in stream){
print(s);
}
print("test end");
}
fromIterable
該方法從一個集合創建Stream
,用法與上面例子大致相同
// 從一個列表創建`Stream`
Stream<int> stream = Stream<int>.fromIterable([1,2,3]);
value
這是Dart2.5 新增的方法,用於從單個值創建Stream
test() async{
Stream<bool> stream = Stream<bool>.value(false);
// await for循環從流中讀取
await for(var i in stream){
print(i);
}
}
監聽 Stream
監聽Stream
,並從中獲取數據也有三種方式,一種就是我們上文中使用的await for
循環,這也是官方推薦的方式,看起來更簡潔友好,除此之外,另兩種方式分別是使用forEach
方法或listen
方法
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
// 使用forEach,傳入一個函數進去獲取並處理數據
stream.forEach((int x){
print(x);
});
使用 listen
監聽
StreamSubscription<T> listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError})
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
stream.listen((x){
print(x);
});
還可以使用幾個可選的參數
test() async{
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
stream = stream.take(5);
stream.listen(
(x)=>print(x),
onError: (e)=>print(e),
onDone: ()=>print("onDone"));
}
onError
:發生Error時觸發onDone
:完成時觸發unsubscribeOnError
:遇到第一個Error時是否取消監聽,默認爲false
Stream 的一些方法
take 和 takeWhile
Stream<T> take(int count)
用於限制Stream
中的元素數量
test() async{
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
// 當放入三個元素後,監聽會停止,Stream會關閉
stream = stream.take(3);
await for(var i in stream){
print(i);
}
}
打印結果:
0
1
2
Stream<T>.takeWhile(bool test(T element))
與 take
作用相似,只是它的參數是一個函數類型,且返回值必須是一個bool
值
stream = stream.takeWhile((x){
// 對當前元素進行判斷,不滿足條件則取消監聽
return x <= 3;
});
skip 和 skipWhile
test() async{
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
stream = stream.take(5);
// 表示從Stream中跳過兩個元素
stream = stream.skip(2);
await for(var i in stream){
print(i);
}
}
打印結果:
2
3
4
請注意,該方法只是從Stream
中獲取元素時跳過,被跳過的元素依然是被執行了的,所耗費的時間依然存在,其實只是跳過了執行完的結果而已。
Stream<T> skipWhile(bool test(T element))
方法與takeWhile
用法是相同的,傳入一個函數對結果進行判斷,表示跳過滿足條件的。
toList
Future<List<T>> toList()
表示將Stream
中所有數據存儲在List中
test() async{
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
stream = stream.take(5);
List <int> data = await stream.toList();
for(var i in data){
print(i);
}
}
屬性 length
等待並獲取流中所有數據的數量
test() async{
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
stream = stream.take(5);
var len = await stream.length;
print(len);
}
StreamController
它實際上就是Stream
的一個幫助類,可用於整個 Stream
過程的控制。
import 'dart:async';
void main() {
test();
}
test() async{
// 創建
StreamController streamController = StreamController();
// 放入事件
streamController.add('element_1');
streamController.addError("this is error");
streamController.sink.add('element_2');
streamController.stream.listen(
print,
onError: print,
onDone: ()=>print("onDone"));
}
使用該類時,需要導入'dart:async'
,其add
方法和sink.add
方法是相同的,都是用於放入一個元素,addError
方法用於產生一個錯誤,監聽方法中的onError
可獲取錯誤。
還可以在StreamController
中傳入一個指定的stream
test() async{
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), (e)=>e);
stream = stream.take(5);
StreamController sc = StreamController();
// 將 Stream 傳入
sc.addStream(stream);
// 監聽
sc.stream.listen(
print,
onDone: ()=>print("onDone"));
}
現在來看一下StreamController
的原型,它有5個可選參數
factory StreamController(
{void onListen(),
void onPause(),
void onResume(),
onCancel(),
bool sync: false})
onListen
註冊監聽時回調onPause
當流暫停時回調onResume
當流恢復時回調onCancel
當監聽器被取消時回調sync
當值爲true
時表示同步控制器SynchronousStreamController
,默認值爲false
,表示異步控制器
test() async{
// 創建
StreamController sc = StreamController(
onListen: ()=>print("onListen"),
onPause: ()=>print("onPause"),
onResume: ()=>print("onResume"),
onCancel: ()=>print("onCancel"),
sync:false
);
StreamSubscription ss = sc.stream.listen(print);
sc.add('element_1');
// 暫停
ss.pause();
// 恢復
ss.resume();
// 取消
ss.cancel();
// 關閉流
sc.close();
}
打印結果:
onListen
onPause
onCancel
因爲監聽器被取消了,且關閉了流,導致"element_1"
未被輸出,"onResume"
亦未輸出
廣播流
如下,在普通的單訂閱流中調用兩次listen
會報錯
test() async{
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), (e)=>e);
stream = stream.take(5);
stream.listen(print);
stream.listen(print);
}
Unhandled exception:
Bad state: Stream has already been listened to.
前面已經說了單訂閱流的特點,而廣播流則可以允許多個監聽器存在,就如同廣播一樣,凡是監聽了廣播流,每個監聽器都能獲取到數據。要注意,如果在觸發事件時將監聽者正添加到廣播流,則該監聽器將不會接收當前正在觸發的事件。如果取消監聽,監聽者會立即停止接收事件。
有兩種方式創建廣播流,一種直接從Stream
創建,另一種使用StreamController
創建
test() async{
// 調用 Stream 的 asBroadcastStream 方法創建
Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), (e)=>e)
.asBroadcastStream();
stream = stream.take(5);
stream.listen(print);
stream.listen(print);
}
使用StreamController
test() async{
// 創建廣播流
StreamController sc = StreamController.broadcast();
sc.stream.listen(print);
sc.stream.listen(print);
sc.add("event1");
sc.add("event2");
}
StreamTransformer
該類可以使我們在Stream
上執行數據轉換。然後,這些轉換被推回到流中,以便該流注冊的所有監聽器可以接收
構造方法原型
factory StreamTransformer.fromHandlers({
void handleData(S data, EventSink<T> sink),
void handleError(Object error, StackTrace stackTrace, EventSink<T> sink),
void handleDone(EventSink<T> sink)
})
handleData
:響應從流中發出的任何數據事件。提供的參數是來自發出事件的數據,以及EventSink<T>
,表示正在進行此轉換的當前流的實例handleError
:響應從流中發出的任何錯誤事件handleDone
:當流不再有數據要處理時調用。通常在流的close()
方法被調用時回調
void test() {
StreamController sc = StreamController<int>();
// 創建 StreamTransformer對象
StreamTransformer stf = StreamTransformer<int, double>.fromHandlers(
handleData: (int data, EventSink sink) {
// 操作數據後,轉換爲 double 類型
sink.add((data * 2).toDouble());
},
handleError: (error, stacktrace, sink) {
sink.addError('wrong: $error');
},
handleDone: (sink) {
sink.close();
},
);
// 調用流的transform方法,傳入轉換對象
Stream stream = sc.stream.transform(stf);
stream.listen(print);
// 添加數據,這裏的類型是int
sc.add(1);
sc.add(2);
sc.add(3);
// 調用後,觸發handleDone回調
// sc.close();
}
打印結果:
2.0
4.0
6.0
總結
與流相關的操作,主要有四個類
Stream
StreamController
StreamSink
StreamSubscription
Stream
是基礎,爲了更方便控制和管理Stream
,出現了StreamController
類。在StreamController
類中, 提供了StreamSink
作爲事件輸入口,當我們調用add
時,實際上是調用的sink.add
,通過sink
屬性可以獲取StreamController
類中的StreamSink
,而StreamSubscription
類則用於管理事件的註冊、暫停與取消等,通過調用stream.listen
方法返回一個StreamSubscription
對象。