Flutter状态管理终极利器-signals的源码解析(监听与通知更新)

598 阅读3分钟

signals版本信息

Flutter项目yaml配置的版本是6.0.2,当前最新的版本。如下所示:

signals: ^6.0.2

signals的项目结构

signals的项目结构如下图所示:

image.png 如图所示:signals相关的有四个目录结构:preact_signals-1.8.3,signals-6.0.2,signals_core-6.0.2,signals_flutter-6.0.2。

  1. preact_signals-1.8.3是preact_signals的dart实现版本。
  2. signals-6.0.2没有具体实现,只是导出signals_core-6.0.2。
  3. signals_core-6.0.2是核心实现。
  4. 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。

image.png signals可以堪称观察者设计模式的集大成者。首先我们先看一下Listenable,如下图所示:

image.png Listenable的子类有Computed和Effect。

还需要再关注一下Node类,注意下图中圈选的属性target,如果源数据变化需要调用值为target的Listenable的notify方法。

image.png 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…

dartsignals.dev/reference/o…

preactjs.com/guide/v10/s…