Dart之Stream异步事件流

在此先插播一句观察者模式
观察者模式常见案例:
RxJava、LiveData、Dart中通过Stream异步事件流构建Widget的Bloc
观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
观察者模式是一种对象行为型模式,
其主要优点如下:

  1. 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
  2. 目标与观察者之间建立了一套触发机制。

它的主要缺点如下。

  1. 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
  2. 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

Stream 就是流的意思,表示发出的一系列的异步数据。可以简单地认为 Stream 是一个异步数据源。它是 Dart 中处理异步事件流的统一 API
集合与Stream
Dart 中,集合(Iterable或Collection)表示一系列的对象。而 Stream (也就是“流”)也表示一系列的对象,但区别在于 Stream 是异步的事件流。比如文件、套接字这种 IO 数据的非阻塞输入流(input data),或者用户界面上用户触发的动作(UI事件)。
集合可以理解为“拉”模式,比如你有一个 List ,你可以主动地通过迭代获得其中的每个元素,想要就能拿出来。而 Stream 可以理解为“推”模式,这些异步产生的事件或数据会推送给你(并不是你想要就能立刻拿到)。这种模式下,你要做的是用一个 listener (即callback)做好数据接收的准备,数据可用时就通知你。
Stream 与 Future
Stream 和 Future 是 Dart 异步处理的核心 API。Future 表示稍后获得的一个数据,所有异步的操作的返回值都用 Future 来表示。但是 Future 只能表示一次异步获得的数据。而 Stream 表示多次异步获得的数据。比如界面上的按钮可能会被用户点击多次,所以按钮上的点击事件(onClick)就是一个 Stream 。简单地说,Future将返回一个值,而Stream将返回多次值。

另外一点, Stream 是流式处理,比如 IO 处理的时候,一般情况是每次只会读取一部分数据(具体取决于实现)。和一次性读取整个文件的内容相比,Stream 的好处是处理过程中内存占用较小。而 File 的readAsStringSync(异步读,返回 Future)或 readAsString(同步读,返回 String)等方法都是一次性读取整个文件的内容进来,虽然获得完整内容处理起来比较方便,但是如果文件很大的话就会导致内存占用过大的问题。
获取Stream的方式:
Stream 有3个工厂构造函数:fromFuture、fromIterable 和 periodic,分别可以通过一个 Future、Iterable或定时触发动作作为 Stream 的事件源构造 Stream

对集合的包装只是简单地模拟异步,定时触发、IO输入、UI事件等现实情况才是真正的异步事件

var data = [1, 2, 3, 4];
var stream = new Stream.fromIterable(data);

2: 使用 Stream 读文件 读文件的方式有多种,其中一种是使用 Stream 获得文件内容。File 的方法 openRead() 返回一个 Stream,List 可以理解为一个 byte array,因为 Dart 中没有 byte 类型。下面的代码将打开当前程序的源代码的 Stream 输入流。

var stream = new File(new Options().script).openRead()

订阅Stream
当你有了一个 Stream 时,最常用的功能就是通过 listen() 方法订阅 Stream 上发出的数据(即事件)。有事件发出时就会通知订阅者。如果在发出事件的同时添加订阅者,那么要在订阅者在该事件发出后才会生效。如果订阅者取消了订阅,那么它会立即停止接收事件。

我们在接收一个输入流的时候要面临几种不同的情况和状态,最基本的是处理收到数据,此外上游还可能出现错误,以及出现错误时是否继续后续数据的处理,最后在输入完成的时候还有一个结束状态。所以 listen 方法的几个参数分别对应这些情况和状态:

onData,处理收到的数据的 callback
onError,处理遇到错误时的 callback
onDone,结束时的通知 callback
unsubscribeOnError,遇到第一个错误时是否停止(也就是取消订阅),默认为false

var data = [1, 2, 3, 4];
var stream = new Stream.fromIterable(data);
//匿名箭头函数
stream.listen((e)=>print(e), onDone: () => print('Done'));
// => 1, 2, 3, 4
// => Done
上面的代码会先打印出从 Stream 收到的每个数字,最后打印一个‘Done’。

当 Stream 中的所有数据发送完时,就会触发 onDone 的调用,但提前取消订阅不会触发 onDone 。在结束的同时(收到 onDone 事件之前),所有的订阅者都被取消了订阅,此时 Stream 上便没有订阅者了。允许对一个已经结束了的 Stream 再添加订阅者(尽管没什么意义),此时只会立刻收到一个 onDone 事件

stream.listen(print, onDone: () {
print('first done');
//listen again
stream.listen(print, onDone:() => print('second done'));
});
// => data: 1,2,3,4,
// => first done
// => no data, because stream is done
// => second done

listen() 方法会返回一个 StreamSubscription 对象,用于提供对订阅的管理控制。onData、onError和onDone 这3个方法分别用于设置(如果listen方法中的参数为null)或覆盖对应的 callback。cancel、pause和resume分别用于取消订阅、暂停和继续。比如,可以在 listen 方法中参数置为 null,接着通过 subscription 对象设置 callback 。此外,cancel 方法也重要,要么一直处理数据直到 stream 结束,要么提前取消订阅结束处理。比如使用 Stream 读文件,为了使资源得到释放,要么读完整个文件,要么使用 subscription 的 cancel 方法取消订阅(即终止后续数据的读取)。可以看出,这里的 cancel 相当于传统意义上的 close 方法。最后,pause和resume方法是尝试向数据源发出暂停和继续的请求,其意义取决于实际情况,并且不保证一定能生效。比如数据源能够支持,或者是带缓冲实现的 stream 才能做到暂停。

var sub = stream.listen(null);
sub.onData(print);
sub.onError((e)=>print('error $e'));
sub.onDone(()=>print('done'));
// => 1, 2, 3, 4, done


var sub = stream.listen(null);
sub.onData((e){
if(e > 2)
sub.cancel();
else
print(e);
});
sub.onDone(()=>print('done'));
// => 1, 2
// no 'done', because stream is cancel.

Stream两种订阅方式 Stream有两种订阅模式:单订阅(single)和多订阅(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者。这就有点类似于消息服务(Message Service)的处理模式。单订阅类似于点对点,在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而广播类似于发布订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在

Stream 默认处于单订阅模式,所以同一个 stream 上的 listen 和其它大多数方法只能调用一次,调用第二次就会报错。但 Stream 可以通过 transform() 方法(返回另一个 Stream)进行连续调用。通过 Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式

单订阅模式会持有数据,多订阅模式如果没有及时添加订阅者则可能丢数据。不过具体取决于 stream 的实现

new Timer(new Duration(seconds:5), ()=>stream.listen(print));
// after 5 second, it output 1,2,3,4
上面的代码利用 Timer 延迟了5秒才订阅 stream,
但仍然输出了数据。因为我们这里的这个 stream 是单订阅模式,
它在有订阅者后才会发出事件。那么多订阅模式就一定会漏掉数据吗?

var bs = stream.asBroadcastStream();
new Timer(new Duration(seconds:5), ()=>bs.listen(print));
// after 5 second, it also output 1,2,3,4
// because asBroadcastStream() is a simple wrap,
// it don't change the source stream's feature

上面我们把原始的单订阅模式转成了多订阅模式的 Stream,
此时可以添加多个订阅者。我们5秒后才在 broadcast stream 上添加了订阅者,
但它依然输出了 1,2,3,4 ,并没有漏掉数据。
这其实是因为 asBroadcastStream() 只是对原始 stream 的封装,
并不改变原始 stream 的实现特性。所以这个 broadcast stream 同样在等待有订阅者之后才发出数据。但是如果一旦有了第一个订阅者,
然后再延迟添加第二个订阅者就会漏数据了。

var bs = stream.asBroadcastStream();
// add first listener
new Timer(new Duration(seconds:5), ()=>bs.listen(print));
// after 5 second, it output 1,2,3,4

// add second listener
new Timer(new Duration(seconds:10), ()=>bs.listen(print));
// after 10 second, nothing output, because stream is done

通过StreamController创建Stream StreamController有两个构造函数,分别用于创建单订阅模式 Stream 和 多订阅模式 Stream。然后可以利用 add()、addError() 和 close() 方法发送事件、发送错误和结束,这三个方法来自 EventSink,是各种 Sink 上的通用方法。

// build single stream
//var controller = new StreamController();

// build broadcast stream多订阅模式
var controller = new StreamController.broadcast();
//send event
controller..add(1)
..add(2)
..add(3)
..add(4);
//send done
controller.close();

var myStream = controller.stream;
new Timer(new Duration(seconds:5), ()=>myStream.listen(print));
//if myStream is single stream, it output 1,2,3,4
//if myStream is broadcast stream, it output nothing, because stream is done.
//单订阅模式
StreamController<int> _counterController = StreamController<int>();
StreamSink<int> get _inAdd => _counterController.sink;
Stream<int> get outCounter => _counterController.stream;

Stream 的集合特性 前面说过,Stream 和一般的集合类似,都是一组数据,只不过一个是异步推送,一个是同步拉取。所以他们都很多共同的方法

stream.any((e) => e > 2).then(print);// stream.any()
print([1,2,3,4].any((e) => e > 2));// iterable.any()
// => true, true

比如 Stream 和 集合 都有 any() 方法,集合是同步的(但是惰性执行,这里因为有 print 调用,所以立刻执行了)并直接返回结果, Stream 上的 any() 方法是异步的,返回的是 Future 。方法本身的含义都是一样的。 上面的代码虽然 stream 的 any 方法在前,但因为是异步的, 所以的输出在后
Stream 和 Iterable 通用的方法

//常见集合方法
stream.first.then(print);
stream.firstWhere((e)=>e>3, defaultValue:()=>0).then(print);
stream.last.then(print);
stream.lastWhere((e)=>e>3, defaultValue:()=>0).then(print);
stream.length.then(print);
stream.isEmpty.then(print);

stream.any((e) => e > 2).then(print);
stream.every((e) => e > 2).then(print);
stream.contains(3).then(print);
stream.elementAt(2).then(print);
stream.where((e) => e >2).listen(print);

stream.skip(2).listen(print);
stream.skipWhile((e) => e < 2).listen(print);
stream.take(2).listen(print);
stream.takeWhile((e)=>e<3).listen(print);

stream.map((e) => e*2).listen(print);
stream.reduce(0, (p, c) => p + c).then(print);
stream.expand((e) => [e, e]).listen(print);

stream.toList().then(print);
stream.toSet().then(print);

注意以上方法同时只能使用一次,因为是单订阅模式。此外,如果方法只有一个返回值,即数据收敛类型的方法,那么返回就是一个 Future。如果是只是数据转换的方法,如 map ,返回的还是一个 Stream,只是数据数据的类型和数量变了。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值