非同步之 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
物件。