前面的章节, 我们探究了Stream、StreamController的源码, 了解了它们的实现。接下来, 我们一起探讨怎样使用它们。
Stream
Stream 是一系列异步事件的序列。其类似于一个异步的 Iterable,不同的是当你向 Iterable 获取下一个事件时它会立即给你,但是 Stream 则不会立即给你而是在它准备好时告诉你。
async*
下面这段代码演示了如何使用async*进行 1 到 10 的相加。
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
await for (final value in stream) {
print("---迭代流,value:$value---");
sum += value;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield i;
}
}
void main() async {
var stream = countStream(10);
var sum = await sumStream(stream);
print(sum); // 55
}
执行结果:
---迭代流,value:1---
---迭代流,value:2---
---迭代流,value:3---
---迭代流,value:4---
---迭代流,value:5---
---迭代流,value:6---
---迭代流,value:7---
---迭代流,value:8---
---迭代流,value:9---
---迭代流,value:10---
55
打印结果可以发现, yield关键字标识发送一个流, await for关键字标识不停地在接收流。
那么, 实际开发过程中, Stream 完成前会出现错误, 我们该怎样处理呢?
流错误事件
Stream 可以像提供数据事件那样提供错误事件。大多数 Stream 会在第一次错误出现后停止,但其也可以提供多次错误并可以在在出现错误后继续提供数据事件。我们只讨论 Stream 最多出现并提供一次错误事件的情况。
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
try {
await for (final value in stream) {
sum += value;
}
} catch (e) {
return -1;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
if (i == 5) {
throw Exception('Intentional exception');
} else {
yield i;
}
}
}
void main() async {
var stream = countStream(10);
var sum = await sumStream(stream);
print(sum); // -1
}
当使用 await for 读取 Stream 时,如果出现错误,则由循环语句抛出,同时循环结束。你可以使用 try-catch 语句捕获错误。下面的示例会在循环迭代到参数值等于 5 时抛出一个错误。
StreamController创建Stream
日常开发中,通常会通过StreamController创建Stream。只需要构造出StreamController对象,通过这个对象的.stream就可以得到Stream。
Stream<int> countStream(int to) {
// 先创建 StreamController
late StreamController<int> controller;
controller = StreamController<int>(onListen: () {
// 当 Stream 被监听时会触发 onListen 回调
for (var i = 0; i < to; i++) {
controller.add(i);
}
controller.close();
});
return controller.stream;
}
Future<int> listenOn(Stream<int> stream) async {
var completer = Completer<int>();
var sum = 0;
// 监听 stream
stream.listen(
(event) {
sum += event;
},
onDone: () => completer.complete(sum),
);
return completer.future;
}
void main() async {
var stream = countStream(10);
// 当注释掉下面这行,控制台也不会打印出 "stream 被监听"
var sum = await listenOn(stream);
print(sum); // 55
}
在创建StreamController的时候传入了一个onListen回调,当流第一次被监听的时候,会触发这个回调,此时会往流里面依次添加多个数据,listenOn方法里拿到这些数据执行相加操作。这里使用了stream的listen的方法进行监听。
常见使用场景
前面章节, 我们一起探讨了流的原理, 下面我们看几个常见的使用场景。
同步流
void _testCStreamSync() {
final StreamController _sc = StreamController(sync: true);
/// 流被监听时触发
_sc.onListen = () {
debugPrint('---😆流被监听时触发😆😆---');
};
/// 监听流,订阅时,流控制器内部在微任务里面调度任务
final StreamSubscription _subscription = _sc.stream.listen((event) {
debugPrint('---😆😆监听流,event:$event😆😆---');
});
/// 添加任务
_sc.add('1');
_sc.add('2');
_sc.add('3');
debugPrint('暂停');
_subscription.pause();
Future.delayed(const Duration(seconds: 3), () {
debugPrint('3秒后 -> 恢复');
_subscription.resume();
});
}
同步流, 订阅后, 添加了三个任务, 然后暂停, 3秒后恢复, 程序的执行顺序是怎样的呢?
打印日志,发现,三个任务按顺序执行完成后,再打印的
暂停, 3s后, 打印3秒后 -> 恢复。
异步流跟scheduleMicrotask结合
void _testCStreamASync() {
final StreamController _sc = StreamController(sync: false);
/// 流被监听时触发
_sc.onListen = () {
debugPrint('---😆流被监听时触发😆😆---');
};
/// 监听流,订阅时,流控制器内部在微任务里面调度任务
final StreamSubscription _subscription = _sc.stream.listen((event) {
debugPrint('---😆😆监听流,event:$event😆😆---');
});
/// 添加任务
_sc.add('1');
_sc.add('2');
_sc.add('3');
scheduleMicrotask(() {
debugPrint('暂停');
_subscription.pause();
Future.delayed(const Duration(seconds: 3), () {
debugPrint('3秒后 -> 恢复');
_subscription.resume();
});
});
}
异步流, 在微任务scheduleMicrotask暂停、3s后恢复。可以看到, 执行第一个任务后,流就暂停了,恢复任务任务后, 剩下的任务继续执行。日志如下:
flutter: ---😆流被监听时触发😆😆---
flutter: ---😆😆监听流,event:1😆😆---
flutter: 暂停
flutter: 3秒后 -> 恢复
flutter: ---😆😆监听流,event:2😆😆---
flutter: ---😆😆监听流,event:3😆😆---
为啥会这样呢?因为流控制StreamController内部执行流的调度时, 通过scheduleMicrotask添加一个微任务到全局微任务队列中。这从前面的源码中, 我们可以清楚地知道。现在我们在外部又添加了一个微任务到全局微任务队列中, 执行暂停、恢复操作。
在 Dart 中,通常只有一个微任务队列。
在Dart中,使用 scheduleMicrotask 可以将任务添加到微任务队列中。微任务是在事件循环的一个阶段内按照先 进先出(FIFO)顺序执行的任务。微任务通常用于在当前事件循环阶段结束之前执行一些任务。
执行流程分析:
代码中, 确实将三个任务('1'、'2' 和 '3')添加到了流控制器 _sc 中。但问题在于,在执行 _sc.add('1') 后,即使任务已经添加到流中,它们仍然需要等待当前的微任务完成,然后才能由流传递给监听器。
让我更详细地解释一下流的工作原理:
- 当调用
_sc.add('1'),'1' 事件被添加到流_sc中,但它不会立即传递给监听器。 - 在当前的微任务(
流控制器里scheduleMicrotask微任务)中,继续添加了 '2' 和 '3' 事件到流_sc中。现在,所有三个事件都在流中排队等待。 - 从
_BufferingStreamSubscription的_add、_sendData、_checkState可知, 系统开了个while循环执行调度_pending!.schedule(this)微任务。 - 此时, 从
_PendingEvents的schedule方法可知, 第一个微任务正在调度执行。 - 然后,我们在
_testCStreamASync方法中, 又用scheduleMicrotask添加了一个新的微任务, 调用_subscription.pause()来暂停监听器。但是这个暂停操作不会立即生效。相反,它会在当前微任务(流控制器里的第一个微任务)完成后生效。 - 目前,第一个微任务仍然在执行中,正在等待处理所有三个事件。只有当当前微(
流控制器里的第一个微任务)任务完成后,才会进入执行下一个微任务。 - 在当前微任务(
流控制器里的第一个微任务)完成后,监听器会处理流中的第一个事件 '1'。然后,因为暂停操作已经生效,后续的 '2' 和 '3' 事件不会被立即处理。 - 3 秒后,
Future.delayed中的任务触发_subscription.resume(),这会恢复监听器的执行。此时,监听器会处理流中的 '2' 和 '3' 事件。
所以,这个行为是由 Dart 中事件循环和异步任务执行的机制导致的,导致在您的代码中首先处理了第一个事件 '1',然后才暂停,最后再处理 '2' 和 '3'。
异步流跟Future.delayed
使用Future.delayed延迟暂停流。
void _testCStreamSync() {
final StreamController _sc = StreamController(sync: false);
/// 流被监听时触发
_sc.onListen = () {
debugPrint('---😆流被监听时触发😆😆---');
};
/// 监听流,订阅时,流控制器内部在微任务里面调度任务
final StreamSubscription _subscription = _sc.stream.listen((event) {
debugPrint('---😆😆监听流,event:$event😆😆---');
});
/// 添加流
_sc.add('1');
_sc.add('2');
_sc.add('3');
_sc.add('4');
_sc.add('5');
_sc.add('6');
_sc.add('7');
Future.delayed(Duration.zero, () {
print('暂停');
_subscription.pause();
Future.delayed(const Duration(seconds: 3), () {
print('3秒后 -> 恢复');
_subscription.resume();
});
});
}
根据Future.delayed文档, 如果持续时间(duration)为0或更短,那么操作将在下一个事件循环迭代之前不会立即完成,而是在所有微任务都运行完毕之后才会完成。
/// If the duration is 0 or less,
/// it completes no sooner than in the next event-loop iteration,
/// after all microtasks have run.
在 Dart 中,微任务是在事件循环中执行的异步任务,通常在当前事件迭代的末尾运行。如果某个操作的持续时间被设置为0或更短,它将不会立即完成,而是会等待当前事件迭代的微任务执行完毕,然后在下一个事件迭代中执行。这确保了微任务的顺序性,因为它们会按照它们排队的顺序依次执行。
所以, 我们看到如下日志:
flutter: ---😆流被监听时触发😆😆---
flutter: ---😆😆监听流,event:1😆😆---
flutter: ---😆😆监听流,event:2😆😆---
flutter: ---😆😆监听流,event:3😆😆---
flutter: ---😆😆监听流,event:4😆😆---
flutter: ---😆😆监听流,event:5😆😆---
flutter: ---😆😆监听流,event:6😆😆---
flutter: ---😆😆监听流,event:7😆😆---
flutter: 暂停
flutter: 3秒后 -> 恢复