šŸ“” Signals

questions:

Preact

Preact signals seem pretty similar to in SolidJS.

import { signal, computed } from "@preact/signals-core";

const s1 = signal("Hello");
const s2 = signal("World");

const c = computed(() => {
  return s1.value + " " + s2.value;
});

computed is kind of, I think, like doing something like this:

const computed = (fn) => {
  const storedValue = useSignal(null)

  useEffect(() => {
    const next = fn()
    storedValue.value = next
  })
  return storedValue
}

idk if thatā€™s actually what theyā€™re doing, but itā€™s basically just using these basic reactive primitives. it might not update until .value is accessed though, not sure EDIT I believe it is lazy until you try to actually access the value, then possibly its marked as outdated but not recalculated when the dependencies change, and then only actually recalculated if you try to access the value

In Preact when you create an effect with useEffect it returns a cleanup function:

const count = signal(1);
const double = computed(() => count.value * 2);
const quadruple = computed(() => double.value * 2);

const dispose = effect(() => {
  console.log("quadruple is now", quadruple.value);
});  

dispose();
count.change = 20;

this basically tells all the things to which the effect was subscribed that they shouldnā€™t try to update it anymore.

This is one thing I was thinking about, it does seem like in their implementation they have a two-way binding between signals and subscribers. Is there realistically any way around that though?

Perf stuff

For the Preact implementation they ran into performance issues with the ā€˜naiveā€™ implementation of subscribers using Sets, so they went with a doubly-linked-list instead. They use this for storing both the sources of a given effect or computed signal and for storing the things which depend on a given signal / computed value. The nodes in the linked list then end up having a source and target property, like so1:

Each Node becomes a sort of quad-linked monstrosity that the dependent can use as a part of its dependency list, and vice versa

Hereā€™s their description of how this works:

During the run, each time a dependency is read, the bookkeeping values can be used to discover whether that dependency has already been seen during this or the previous run. If the dependency is from the previous run, we can recycle its Node. For previously unseen dependencies we create new Nodes. The Nodes are then shuffled around to keep them in reverse order of use. At the end of the run we walk through the dependency list again, purging Nodes that are still hanging around with the ā€œunusedā€ flag set. Then we reverse the list of remaining nodes to keep it all neat for later consumption.

This delicate dance of death allows us to allocate only one Node per each dependency-dependent pair and then use that Node indefinitely as long as the dependency relationship exists. If the dependency tree stays stable, memory consumption also stays effectively stable after the initial build phase. All the while dependency lists stay up to date and in order of use. With a constant O(1) amount of work per Node. Nice!2

It sounds like this maintains a single global linked list of these dependency \leftrightarrow dependent relationships. EDIT: No, instead there is a linked list stored for each evaluation context which is basically like running the cb in an effect or a computed signal.

One interesting feature of the computed signals is that in the dependency node list there is a version number stored for each dependency (a primitive or computed signal). This lets dependents record the last version they saw and thereby avoid recomputing if none of their inputs / dependencies are different (this is much faster that doing a compare-by-value thing on the actual values themselves, and is also more GC friendly because Signal values could be objects which we wouldnā€™t want to keep hanging around indefinitely).

A tour of the source

The code for the Signals implementation in Preact is located in a separate repo from the main project. The core implementation is not a huge amount of code (578SLOC at the time of writing) so we can easily read through to divine some secrets.

First here is the type declaration for the linked-list node object:

packages/core/src/index.ts:L17-L43

// A linked list node used to track dependencies (sources) and dependents (targets).
// Also used to remember the source's last version number that the target saw.
type Node = {
    // A node may have the following flags:
    //  NODE_FREE when it's unclear whether the source is still a dependency of the target
    //  NODE_SUBSCRIBED when the target has subscribed to listen change notifications from the source
    _flags: number;

    // A source whose value the target depends on.
    _source: Signal;
    _prevSource?: Node;
    _nextSource?: Node;

    // A target that depends on the source and should be notified when the source changes.
    _target: Computed | Effect;
    _prevTarget?: Node;
    _nextTarget?: Node;

    // The version number of the source that target has last seen. We use version numbers
    // instead of storing the source value, because source values can take arbitrary amount
    // of memory, and computeds could hang on to them forever because they're lazily evaluated.
    _version: number;

    // Used to remember & roll back the source's previous `._node` value when entering &
    // exiting a new evaluation context.
    _rollbackNode?: Node;
};

There is a global called evalContext which holds a reference to the current evaluation context:

packages/core/src/index.ts:L106-L107

// Currently evaluated computed or effect.
let evalContext: Computed | Effect | undefined = undefined;

Basically whenever doing something that could result in new subscriptions this will be set to the value of the current Computed or Effect object so that inside of the .value calls on e.g.Ā a Signal weā€™ll know where to go to register that dependency.

TODO


  1. image from https://preactjs.com/blog/signal-boosting/ā†©ļøŽ

  2. Also from https://preactjs.com/blog/signal-boosting/. Do we ibid on the web?ā†©ļøŽ