本文整个系列文章有:
- #[Flutter小试牛刀] 写一个低配版的signals
- #[Flutter小试牛刀] 低配版signals,添加多层监听链
- #[Flutter小试牛刀] 低配版signals,添加局部刷新
- #[Flutter小试牛刀] 低配版signals,添加batch
在上篇文章里,已经写了一个减配版的signals,且添加了局部刷新的功能,这已经能够满足大部分的需求。但是某些时候,多个值的改变都会刷新某一部分UI,如果连续改动它们的值就会造成重复刷新的问题;
我们可以看到下面signals的一段代码:
final a = signal(0);
final b = signal(1);
final c = computed(() => a.value + b.value);
effect((){
print("c is ${c.value}");
});
a.value ++;
b.value ++;
当a值与b值的改变时都会影响c值的改变。我们同时改变了a 和 b的值,effect函数回调了两次。但某些情况下为了性能,我们只需要它回调一次就够了。
为了解决这个问题,signals添加了batch函数:
final a = signal(0);
final b = signal(1);
final c = computed(() => a.value + b.value);
effect((){
print("c is ${c.value}");
});
batch((){
a.value++;
b.value++;
});
上面带代码基于之前的代码添加了些许改进,在batch函数里对值进行修改,effect函数只会触发一次改动。
那么如何在我们简化版的signals里实现batch功能呢?
具体的实现思路如下:
- 记录batch开始
- 触发batch开始函数
- 触发batch函数过程中如果有刷新回调则将回调函数添加到batch回调列表。
- 记录batch结束
- 触发batch回调函数列表
有了上述的思路开始改造代码:
UseBatch? _batchNode;
class UseBatch extends Node {
final VoidCallback fn;
final Set<void Function()> _listeners = {};
UseBatch(this.fn);
@override
void dispose() {
_listeners.clear();
_unsubscribe();
}
@override
void notifyListeners() {
for (var listener in _listeners) {
listener.call();
}
}
@override
void addListener(void Function() listener) {
_listeners.add(listener);
}
@override
void removeListener(void Function() listener) {
_listeners.remove(listener);
}
void call() {
//判断是否已经处在batch,如果是则停止执行
if (_batchNode != null) {
return;
}
//将batch节点设置成自己
_batchNode = this;
try {
//触发batch函数
fn.call();
} catch (e) {
rethrow;
} finally {
//重置batch函数
_batchNode = null;
//触发batch回调函数列表
notifyListeners();
//清理
dispose();
}
}
}
abstract class MixinUseState<W extends StatefulWidget> extends State<W> {
...
//batch函数
void batch(VoidCallback fn) {
UseBatch(fn)();
}
void _notifyListeners() {
//判断当前是否在batch函数下,如果是则将UI改变暂存,因为listeners是set,相同的回调函数只会回调一次
if (_batchNode != null) {
_batchNode?.addListener(_notifyListeners);
return;
}
setState(() {});
}
}
该改动的核心是我们知道所有触发UI改动的函数是 _notifyListeners ,我们可以在触发_notifyListeners前判断是否有 _batchNode ,如果有则延后触发。