从头开始构建反应库信号反应和衍生结论

2025-05-26

从头开始构建反应式库

信号

反应和衍生物

结论

在上一篇文章《细粒度响应式实践入门》中,我通过示例解释了细粒度响应式背后的概念。现在,我们来学习如何自己构建一个响应式库。

有些东西在实际操作时总显得有些神奇,但实际上机制上并没有那么复杂。响应式之所以如此神奇,是因为一旦部署到位,即使在动态场景下也能自行处理。这正是真正的声明式方法的优势,因为只要契约得到遵守,具体实现就无关紧要。

我们将要构建的反应式库不会具有MobXVueSolid等的所有功能,但它应该作为一个很好的例子来让您了解它的工作原理。

信号

信号是我们响应式系统的核心,也是我们开始的正确起点。它包含 getter 和 setter 方法,所以我们可以从以下代码开始:

export function createSignal(value) {
  const read = () => value;
  const write = (nextValue) => value = nextValue;
  return [read, write];
}
Enter fullscreen mode Exit fullscreen mode

这目前还没有做太多的事情,但我们可以看到我们现在有一个简单的容器来保存我们的价值。

const [count, setCount] = createSignal(3);
console.log("Initial Read", count());

setCount(5);
console.log("Updated Read", count());

setCount(count() * 2);
console.log("Updated Read", count());
Enter fullscreen mode Exit fullscreen mode

那么我们还缺少什么呢?管理订阅。信号是事件发射器。

const context = [];

function subscribe(running, subscriptions) {
  subscriptions.add(running);
  running.dependencies.add(subscriptions);
}

export function createSignal(value) {
  const subscriptions = new Set();

  const read = () => {
    const running = context[context.length - 1];
    if (running) subscribe(running, subscriptions);
    return value;
  };

  const write = (nextValue) => {
    value = nextValue;

    for (const sub of [...subscriptions]) {
      sub.execute();
    }
  };
  return [read, write];
}
Enter fullscreen mode Exit fullscreen mode

这里有一些需要解开的内容。我们主要管理两件事。在文件顶部,有一个全局context堆栈,用于跟踪任何正在运行的反应或派生。此外,每个信号都有自己的subscriptions列表。

这两件事构成了自动依赖跟踪的基础。执行时,Reaction 或 Derivation 会将自身推送到context堆栈上。它会被添加到subscriptions执行期间读取的任何信号的列表中。我们还会将信号添加到运行上下文中,以便进行清理工作,这将在下一节中介绍。

最后,在 Signal 写入时,除了更新值之外,我们还执行所有订阅。我们克隆列表,以便在执行过程中添加的新订阅不会影响本次运行。

这是我们完成的信号,但它只是方程的一半。

反应和衍生物

现在你已经看到了一半,或许能猜出另一半是什么样子了。让我们创建一个基本的反应(或效果)。

function cleanup(running) {
  for (const dep of running.dependencies) {
    dep.delete(running);
  }
  running.dependencies.clear();
}

export function createEffect(fn) {
  const execute = () => {
    cleanup(running);
    context.push(running);
    try {
      fn();
    } finally {
      context.pop();
    }
  };

  const running = {
    execute,
    dependencies: new Set()
  };

  execute();
}
Enter fullscreen mode Exit fullscreen mode

我们在这里创建的是推送到上下文的对象。它包含 Reaction 监听的依赖项(信号)列表,以及我们跟踪并重新运行的函数表达式。

每次循环,我们都会取消订阅该 Reaction 的所有信号,并清除依赖列表以重新开始。这就是我们存储反向链接的原因。这使我们能够在每次运行时动态创建依赖项。然后,我们将该 Reaction 压入堆栈并执行用户提供的函数。

这 50 行代码可能看起来不多,但我们现在可以重新创建上一篇文章中的第一个演示。

console.log("1. Create Signal");
const [count, setCount] = createSignal(0);

console.log("2. Create Reaction");
createEffect(() => console.log("The count is", count()));

console.log("3. Set count to 5");
setCount(5);

console.log("4. Set count to 10");
setCount(10);
Enter fullscreen mode Exit fullscreen mode

添加一个简单的派生并不需要太多复杂度,只需要使用与 中基本相同的代码即可createEffect。在像 MobX、Vue 或 Solid 这样的真正的响应式库中,我们会内置一个推/拉机制,并跟踪响应图以确保我们没有做额外的工作,但出于演示目的,我将只使用 Reaction。

注意:如果您有兴趣实现他的推/拉方法的算法,我建议您阅读《成为完全反应式:对 MobX 的深入解释》

export function createMemo(fn) {
  const [s, set] = createSignal();
  createEffect(() => set(fn()));
  return s;
}
Enter fullscreen mode Exit fullscreen mode

让我们重新创建条件渲染示例:

console.log("1. Create");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const [showFullName, setShowFullName] = createSignal(true);

const displayName = createMemo(() => {
  if (!showFullName()) return firstName();
  return `${firstName()} ${lastName()}`
});

createEffect(() => console.log("My name is", displayName()));

console.log("2. Set showFullName: false ");
setShowFullName(false);

console.log("3. Change lastName");
setLastName("Legend");

console.log("4. Set showFullName: true");
setShowFullName(true);
Enter fullscreen mode Exit fullscreen mode

lastName如您所见,因为我们每次都会构建依赖图,所以当我们不再监听它时,我们不会重新执行更新时的推导。

结论

这些就是基础知识。当然,我们的库没有批处理、自定义处理方法,也没有针对无限递归的保护措施,而且并非完美无缺。但它包含了所有核心部分。2010年代初的KnockoutJS等库就是这样运作的。

出于上述所有原因,我不建议使用这个库。但是,只需大约 50 行代码,你就拥有了一个简单的响应式库的所有功能。而且,当你考虑到可以用它建模多少行为时,你应该就能理解为什么像SvelteSolid这样带有编译器的库可以生成如此小的包。

如此少的代码却蕴含如此强大的功能。您可以真正地用它来解决各种各样的问题。只需几行代码,它就能成为您选择的框架的状态库,再多几十行代码,它就能成为框架本身

希望通过这个练习,您现在可以更好地理解和欣赏细粒度反应库中的自动跟踪如何工作,并且我们已经揭开了其中的一些神秘面纱。


想知道 Solid 如何利用这一点,并将其打造成为一个完整的渲染库。请查看SolidJS:渲染响应式

文章来源:https://dev.to/ryansolid/building-a-reactive-library-from-scratch-1i0p
PREV
SolidJS 正式发布:通往 1.0 的漫漫长路
NEXT
细粒度反应性实践介绍参与者反应性生命周期同步执行结论