Dart 語言非同步之Stream詳解

程式設計之路從0到1發表於2019-09-15

非同步之 Stream 詳解

關於Dart 語言的Stream 部分,應該回到語言本身去尋找答案,許多資料在Flutter框架中囫圇吞棗式的解釋Stream,總有一種讓人云山霧罩的感覺,事實上從Dart語言本身去了解Stream並不複雜,接下來就花點時間好好學習一下Stream吧!

StreamFuture都是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物件。

關注我的公眾號:程式設計之路從0到1

程式設計之路從0到1

相關文章