Dart 語言Stream詳解

異步之 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章