signals是继privider,riverpod状态管理器之后的又一个热门; 它运用方便,理解简单,特别是已经对前端signals框架比较熟悉的同学。
文本旨在研究signals的原理,复刻一个"够用就行"的低配版状态管理框架。
signals 的核心用法就是 signal 和 computed 方法。
dart
final a = signal('a');
final b = signal('b');
final c = computed(() => a.value + b.value);
final dispose = effect((){
print("c is ${c.value}");
});
expect(c.value, 'ab');
a.value = 'aa';
expect(c.value, 'aab');
dispose();
从上面的代码可以看出,我们在改变a的值后,c的值也随之改变。当c值改变时会触发effect的回调。 在flutter中我们可以将effect方法看作是 build方法,返回Wdiget树,那么当我们改变某个signal值时就会触发build重绘widget。
有意思的是,effect方法实际上监听的只是c的值,之所以改变a会触发c的改变是因为competed方法里c的值用到了a.value。
因此singals的核心思想就是这种链式监听。
初学者会感觉这像是一种魔法,因为c.value并没有传入任何的变量,那c和a,b等是如何实现关联监听的呢? 从singals源码中可以一窥究竟:
dart
@override
T get value {
final node = addDependency(this);
if (node != null) {
node.version = this.version;
}
return this.internalValue;
}
Node? addDependency(ReadonlySignal signal) {
if (evalContext == null) {
return null;
}
var node = signal.node;
if (node == null || node.target != evalContext) {
node = Node()
..version = 0
..source = signal
..prevSource = evalContext!.sources
..nextSource = null
..target = evalContext!
..prevTarget = null
..nextTarget = null
..rollbackNode = node;
if (evalContext!.sources != null) {
evalContext!.sources!.nextSource = node;
}
evalContext!.sources = node;
signal.node = node;
if ((evalContext!.flags & TRACKING) != 0) {
signal.subscribeToNode(node);
}
return node;
} else if (node.version == -1) {
node.version = 0;
if (node.nextSource != null) {
node.nextSource!.prevSource = node.prevSource;
if (node.prevSource != null) {
node.prevSource!.nextSource = node.nextSource;
}
node.prevSource = evalContext!.sources;
node.nextSource = null;
evalContext!.sources!.nextSource = node;
evalContext!.sources = node;
}
return node;
}
return null;
}
以上是Signal类中截取的获取value的方法,可以看出它调用了addDependency返回了一个node。而在addDependency则对每个Signal初始化了一个Node对象用来管理Signal之间的关系,从代码实现来看Node对象之间是通过双向链表实现关联的。且从代码中还可以发evalContext这个引用频繁出现,且是一个全局变量。
我们再看看Computed的实现:
dart
@override
T get value {
if ((flags & RUNNING) != 0) {
throw Exception('Cycle detected');
}
final node = addDependency(this);
internalRefresh();
if (node != null) {
node.version = version;
}
if ((flags & HAS_ERROR) != 0) {
throw error!;
}
return _internalValue;
}
bool internalRefresh() {
this.flags &= ~NOTIFIED;
if ((this.flags & RUNNING) != 0) {
return false;
}
if ((this.flags & (OUTDATED | TRACKING)) == TRACKING) {
return true;
}
this.flags &= ~OUTDATED;
if (this.internalGlobalVersion == globalVersion) {
return true;
}
this.internalGlobalVersion = globalVersion;
this.flags |= RUNNING;
if (version > 0 && !needsToRecompute(this)) {
this.flags &= ~RUNNING;
return true;
}
final prevContext = evalContext;
try {
prepareSources(this);
evalContext = this;
final val = this.fn();
if (!_isInitialized ||
(flags & HAS_ERROR) != 0 ||
_internalValue != val ||
version == 0) {
internalValue = val;
flags &= ~HAS_ERROR;
version++;
}
} catch (err) {
error = err;
flags |= HAS_ERROR;
version++;
}
evalContext = prevContext;
cleanupSources(this);
flags &= ~RUNNING;
return true;
}
代码很长,我们可以看到Computed和Signal的value对象一样调用了addDependency,之后调用了internalRefresh方法,在internalRefresh方法里我们又看到evalContext的身影,我们发现在调用 final val = this.fn();前它:evalContext = this;调用结束后又重制回来了:evalContext = prevContext;
从上面代码我们不难看出,Signals的魔法就在这里通过全局变量evalContext临时存储上下文信息,执行结束后又重置回来。 魔法就此揭开了。也许有人会问,这样设置全局变量不会有并发问题吗?答案是不会,因为dart的运行时isolate是单线程模型,它运行的实际上是按照顺序执行代码块,这里不详细展开讲解,可以自行查isolate的执行原理。
OK,那么我们如何在flutter中实现一个简单的像signals里signal方法和computed方法呢?首先我们也需要写一个Node用来管理调用链之间的关系,这里为了简单只设计两层的调用链,也就是computed只允许signal,不允许computed嵌套使用。他们的关系就先用树结构进行管理:
dart
mixin class Node {
//关联的子节点用Set是为了避免节点重复加入
final Set<Node> children = {};
//添加子节点
void addChild(Node node) {
children.add(node);
}
//删除子节点
void removeChild(Node node) {
children.remove(node);
}
//通知子节点改变
void notify() {
for (var child in children) {
child.notify();
}
}
//销毁
void dispose() {
children.clear();
}
}
有了节点,我们就先写Singal,这里为了和Signal区分就用UseValue代替,为了方便就直接用Flutter的ValueNotifier:
dart
typedef DisposeFn = void Function();
typedef ComputeFn<T> = T Function();
Node? childNode;
class UseValue<T> extends ValueNotifier<T> with Node {
final DisposeFn? disposeFn;
UseValue(super.value, {this.disposeFn});
@override
T get value {
if (childNode != null) {
addChild(childNode!);
}
var value = super.value;
return value;
}
@override
void dispose() {
disposeFn?.call();
super.dispose();
}
@override
void notifyListeners() {
super.notifyListeners();
notify();
}
}
在以上代码中我们定义了一个childNode全局对象,这个跟signals里的evelContext是一个作用,用于关联节点之间的关系。在T get value方法里,会去判断childNode是否为空,如果不为空则会将它加入到自己的children里,如果自己的之值发生改变则会去通知children也去改变。
有了Signal再写Computed,为了区分,我这里用UseComputed代替:
dart
class UseComputed<T> extends ChangeNotifier with Node {
final ComputeFn<T> computeFn;
UseComputed(this.computeFn);
T get value {
var oldChildNode = childNode;
try {
childNode = this;
var computeValue = computeFn();
return computeValue;
} finally {
childNode = oldChildNode;
}
}
@override
void notify() {
notifyListeners();
}
}
从上面代码中 T get value 的实现可以看出,我们会把全局变量 childNode暂时缓存并替换成自己,执行完回调函数后又重置回去。
我们再写一个State的实现:
dart
mixin MixinUseState<W extends StatefulWidget> on State<W> {
final Set<DisposeFn> _disposeFnSet = {};
@override
void dispose() {
for (var disposeFn in _disposeFnSet) {
disposeFn.call();
}
super.dispose();
}
void _notifyChanged() {
setState(() {});
}
//类似 signals里的 signal 方法
UseValue<T> use<T>(T value,{bool addListener}) {
var useValue = UseValue<T>(value);
if(addListener){
useValue.addListener(_notifyChanged);
}
_disposeFnSet.add(useValue.dispose);
return useValue;
}
//类似 signals里的 computed 方法
UseComputed<T> computed<T>(ComputeFn<T> computeFn) {
var useComputed = UseComputed(computeFn);
_disposeFnSet.add(useComputed.dispose);
useComputed.addListener(_notifyChanged);
return useComputed;
}
}
在上面代码中,还添加了自动dispose的功能,调用use和compted时会将dispose方法放入到 _disposeFnSet中。
再写个Demo:
dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with MixinUseState {
late final c1 = use(0);
late final c2 = use(2);
late final counter = computed(() {
return c1.value + c2.value;
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${counter.value}',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),),
floatingActionButton: FloatingActionButton(
onPressed: () {
c1.value++;
},
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
在测试代码中,定义了 c1,c2,c3,首先c1和c2类似于signal,而c3则是computed,我们在改变c1.value时会触发c3的改变,进行重新计算,并刷新UI。
自此这个简单的demo就实现完成了,但signals的功能远不止这些,例如batch可以同时改变多个值减少刷新次数,containner,类似riverpod里的family等。