signals版本信息
Flutter项目yaml配置的版本是6.0.2,当前最新的版本。如下所示:
signals: ^6.0.2
signals的项目结构
signals的项目结构如下图所示:
如图所示:signals相关的有四个目录结构:preact_signals-1.8.3,signals-6.0.2,signals_core-6.0.2,signals_flutter-6.0.2。
- preact_signals-1.8.3是preact_signals的dart实现版本。
- signals-6.0.2没有具体实现,只是导出signals_core-6.0.2。
- signals_core-6.0.2是核心实现。
- signals_flutter-6.0.2是signals_core对flutter的扩展。
单步调试来了解signals的原理
以下面的示例代码作为调试的示例:
final name = signal("Jane");
final surname = signal("Doe");
final fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
final dispose = effect(() {
print("name is ${name.value}");
print("fullName is ${fullName.value}");
});
// Updating one of its dependencies will automatically trigger
// the effect above, and will print "John Doe" to the console.
name.value = "John";
print(fullName.value);
当代码断点暂停到第6行的时候,name和surname版本号是0,而fullName版本号是1。当代码断点暂停到第13行的时候,因为name的value改变了,所以name和fullName的版本号个字更加1。
signals可以堪称观察者设计模式的集大成者。首先我们先看一下Listenable,如下图所示:
Listenable的子类有Computed和Effect。
还需要再关注一下Node类,注意下图中圈选的属性target,如果源数据变化需要调用值为target的Listenable的notify方法。
Node是一个双向链表代替之前的Set集合,这种改变主要是性能的考虑,双向链表不仅有序,而且能在O(1)的时间复杂度删除前驱节点。
Node是如何被添加的
Signal的主要相关逻辑代码如下:
@override
T get value {
final node = addDependency(this);
if (node != null) {
node.version = this.version;
}
return this.internalValue;
}
/// Set the current value by a setter
set value(T val) => set(val);
/// Set the current value by a method
bool set(
T val, {
/// Skip equality check and update the value
bool force = false,
}) {
if (force || !isInitialized || val != this.internalValue) {
internalSetValue(val);
return true;
}
return false;
}
@internal
void internalSetValue(T val) {
if (batchIteration > 100) {
throw Exception('Cycle detected');
}
this.internalValue = val;
this.version++;
globalVersion++;
startBatch();
try {
for (var node = targets; node != null; node = node.nextTarget) {
node.target.notify();
}
} finally {
endBatch();
}
}
如何通知相关的Listenable,signal中的数据发生了变化:其实很简单看上面代码中的value的set方法,最终调用set方法,方法内设置的新值是否和原来的值不相等,条件满足会调用internalSetValue方法;internalSetValue方法内部是按照targets的node链表依次调用Listenable的notify方法。所以在 signals里,会利用Node对象来通知存储在targets列表中的所有依赖者,当信号的值发生改变时,会遍历依赖者列表,并根据_version对比结果来触发更新。这样就完成了自动更新的逻辑。
Node链表如何串联起来呢?如下面的示例代码:
@internal
Node? addDependency(ReadonlySignal signal) {
if (evalContext == null) {
return null;
}
var node = signal.node;
if (node == null || node.target != evalContext) {
/**
* `signal` is a new dependency. Create a new dependency node, and set it
* as the tail of the current context's dependency list. e.g:
*
* { A <-> B }
* ↑ ↑
* tail node (new)
* ↓
* { A <-> B <-> C }
* ↑
* tail (evalContext._sources)
*/
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;
// Subscribe to change notifications from this dependency if we're in an effect
// OR evaluating a computed signal that in turn has subscribers.
if ((evalContext!.flags & TRACKING) != 0) {
signal.subscribeToNode(node);
}
return node;
} else if (node.version == -1) {
// `signal` is an existing dependency from a previous evaluation. Reuse it.
node.version = 0;
/**
* If `node` is not already the current tail of the dependency list (i.e.
* there is a next node in the list), then make the `node` the new tail. e.g:
*
* { A <-> B <-> C <-> D }
* ↑ ↑
* node ┌─── tail (evalContext._sources)
* └─────│─────┐
* ↓ ↓
* { A <-> C <-> D <-> B }
* ↑
* tail (evalContext._sources)
*/
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;
}
// We can assume that the currently evaluated effect / computed signal is already
// subscribed to change notifications from `signal` if needed.
return node;
}
return null;
}
@override
void subscribeToNode(Node node) {
ReadonlySignal.internalSubscribe(this, node);
}
@internal
static void internalSubscribe(ReadonlySignal signal, Node node) {
if (signal.targets != node && node.prevTarget == null) {
node.nextTarget = signal.targets;
if (signal.targets != null) {
signal.targets!.prevTarget = node;
}
signal.targets = node;
}
}
从上面的代码中的addDependency方法可以看出node的添加有两种情况:1.node是新的,直接放到node的尾部节点。2.复用node的情况,把node的前驱节点和node后继节点首尾相连,然后将node节点放到node的尾部节点。这样就完成了自动状态绑和自动依赖跟踪。
总结
Signals提供细粒度的反应系统,可自动跟踪依赖关系并在不再需要时释放它们。不仅简单,而且高效。希望本文对您有所帮助,希望您编码愉快。
参考资料
signals: pub.dev/packages/si…
介绍signals:preactjs.com/blog/introd…