我们使用一个第三方库,不应只看有没有名气,star 多不多,功能强不强大,库的实现原理是否高大上,更应该看它
-
解决了什么问题
-
是否符合我们的项目需求
-
有没有带来新问题
先说结论,signal 在 flutter ui 更新意义不大。
Signal 简介
基于 preactjs.com/blog/signal… 实现了 dart 版。
主打最大的优势是 高性能的颗粒度更新。
Preact Signals
Preact Signals 是 Preact 团队(React 的轻量替代框架)推出的一种 响应式状态管理机制,核心思想是用极小代价实现“自动追踪依赖 + 精准更新”。
数据处理上的细粒度更新
郭神之前有篇文章已经介绍了 signal 的原理:guoshuyu.cn/home/wx/Flu… 。
flutter ui 上的细粒度更新
flutter 上的细粒度体现在将 element 和 signal 直接关联. 无需经过 Widget 层的 build,直接通知关联的 element 更新。
看这个例子,Signal 通过 Watch 包裹 Widget 来达到最细粒度的更新。
class _CounterExampleState extends State<CounterExample> {
late final Signal<int> counter = Signal(0);
void _incrementCounter() {
counter.value++;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Watch((context) {
return Text(
'$counter',
style: Theme.of(context).textTheme.headlineMedium,
);
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
这个例子里:
counter 会直接关联 Text 的 element。counter 更新时,他直接标记 Text 的 Element 为脏,触发下一帧更新。这样直接绕过 Widget build 直达 Element,听着很高效是不是。但是其实它还是会触发 build,和 setState 达到的效果是一模一样的, setState 本质上也是标脏 ,所以, Watch 和 ValueListenerBuilder 对于 ui 的细腻度更新是 没有任何区别的。
唯一的区别,只是写法不同, Watch 不需要传入任何 value:
// 不需要任何输入,Watch 会自动关联 counter
Watch((context) {
return Text(
'$counter',
style: Theme.of(context).textTheme.headlineMedium,
);
})
// 但 ValueListenableBuilder 需要先传入 counterNotifier 手动关联
ValueListenableBuilder(value:counterNotifier,builder:)
实际的开发场景,状态管理往往是页面为单位的,因为数据会被页面里的很多 Widget 用到,以及一些乱七八糟的逻辑判断。 比如:Signal 持有的会是一个聚合的状态 PageState.
// 聚合的状态对象
class PageState {
final String title;
final bool isLoading;
final int counter;
// ... 其他几十个属性
// 构造函数和 copyWith 方法...
PageState({this.title = '', this.isLoading = false, this.counter = 0});
PageState copyWith({String? title, bool? isLoading, int? counter}) {
return PageState(
title: title ?? this.title,
isLoading: isLoading ?? this.isLoading,
counter: counter ?? this.counter,
);
}
}
// 单一数据源
final pageState = signal(PageState(title: 'My App'));
// 从单一数据源派生出细粒度的 computed signals
final title = computed(() => pageState.value.title);
final isLoading = computed(() => pageState.value.isLoading);
final counter = computed(() => pageState.value.counter);
如果要实现精准的细粒度就得这么写, 每个控件用: Watch((context) 嵌套一下。
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 这个 AppBar 的标题只关心 title 的变化
title: Watch((context) => Text(title.value)),
),
body: Center(
child: Column(
children: [
// 这个加载指示器只关心 isLoading 的变化
Watch((context) {
return isLoading.value ? CircularProgressIndicator() : SizedBox();
}),
// 这个 Text 只关心 counter 的变化
Watch((context) => Text('Count: ${counter.value}')),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 假设我们只更新 counter
pageState.value = pageState.value.copyWith(counter: pageState.value.counter + 1);
},
),
);
}
}
那么我想问,
-
把每个 Watch((context) 替换成 ValueNotifierBuilder 吗?
-
把
final title = computed(() => pageState.value.title);换成ValueNotifier
有什么不一样吗? 这也是响应式,也是精准细粒度更新,只不过每个 ValueNotifierBuilder 多调一次 build 而已。
有人说, 我可以每个属性都写成 signal, so? 无非就是省去了 几行 computed,Watch 还是少不了,我同样可以每个属性都写成 Notifier。
从数据上看,我不认可官方的 benchmark 数据,有点射完箭再画靶子的意思。
性能敏感的场景我们肯定没那么呆直接嵌套,而是想上述一样用 ValueNotifierBuilder 来实现局部的频繁更新,基本能解决90%的问题。而 Widget 本身的 build 机制已经是足够高效,为了省这么点性能,引入这样的一个有点 违背 Widget 机制的库,并且要达到完美的细粒度更新,必须嵌套写 Watch((context)。
违背 Widget 哲学
Watch 的背后做了很多操作: 关联 element 和 signal,建立二者的联系,以及触发其惰性版本依赖。
违背了: 保持 build 的纯净 的单一职责。
signals.dart 解决的痛点
所以 signals.dart 最大的价值就是其惰性版本依赖机制,处理数据的高效。
final a = createSignal(1);
final b = createSignal(2);
final sum = createComputed(() => a.value + b.value);
final doubled = createComputed(() => sum.value * 2);
首先是,写法麻烦,只能被迫用 signal 包装,每个属性都写一个 signal?或者每个地方都写成 createComputed, 你不嫌麻烦吗?如果你偷懒不分这么细的粒度,那你为什么要用它? 然后是 侵入性超强 ,后期想换,每个属性都要去改?
极端场景下,确实能有不错的优化效果,但我非要用这个库来解决吗?我自己写一个策略来控制它不行吗。
当你修改 a.value = 2 时,影响了太多人。你想修改 b 但又不太敢修改,因为会影响上下游的链路,要改的话,整个链路模块最好一起回归测试, 叠甲,我是说最好,他当前可以单独测试, 如果你能拍着胸脯说100%不出问题就行。
虽然 signal 提供了可视化的调试工具,但只是减缓了上述问题,一堆 signal 依赖耦合在一起,眼花缭乱。
总结
所以 signal 主打的细粒度更新,真的戳到你的痛点了吗?