利用信号进行本地思考

2025-06-10

利用信号进行本地思考

作为 SolidJS 的创建者,我在设计这个库时深受 React 的影响。尽管人们可能觉得它很不靠谱,但真正启发我的并非虚拟 DOM 或 JSX 之类的技术,而是它的一些原则。这些技术或许展现了 React 的能力,甚至可能将其定义为一种解决方案,但并非它的遗产。

相反,诸如单向流、组合和显式突变之类的特性,持续影响着我们构建用户界面的方式。当我看到 Solid 的核心响应式技术 Signals 被整个生态系统的库所采用时,至关重要的是,我们不要忘记从 React 中汲取的教训。


思维的局部性

当地种植的农产品

React 的真正魔力在于,当你将它的所有设计原则结合起来时,你无需查看其余代码就可以推断出给定组件的行为。

这使得新人无需了解整个代码库就能高效工作。它允许独立构建功能,而不会影响应用程序的其余部分。它让你可以回顾一年前编写的代码,并理解它的功能。

这些都是构建软件的不可思议的力量。值得庆幸的是,这些力量并不局限于 React 的技术选择。


传递价值

React Docs 中的组件图

单向流可能是实现局部性思维最重要的部分。通常,这始于不变性,因为传递可变的内容后,调用后就永远无法信任它了。

let obj = {}
someFunction(obj)

// Is this true?
console.log(obj.value === undefined)

// You can't tell without looking at the code for `someFunction`
Enter fullscreen mode Exit fullscreen mode

然而,这并不需要真正的不变性。它需要的是读/写隔离。或者换句话说,显式地改变。传递值的行为不应该隐式地赋予改变它的能力。

因此,任何离开我们组件的接口,无论是用于原始组合(例如自定义信号)还是 JSX,都不应默认同时传递值和 setter。我们在 SolidJS 中鼓励这样做,通过在 JSX 中使用按值传递的方法,并提供默认读/写分离的原始操作。

// [read, write]
const [title, setTitle] = createSignal("title");

// `title()` is the value, `SomeComponent` can't change `title`
<SomeComponent title={title()} />

// Now `SomeComponent` can update it
<SomeComponent title={title()} updateTitle={setTitle} />
Enter fullscreen mode Exit fullscreen mode

Svelte Runes 采取了另一种方法来实现这一点,即将变量访问编译为 Signal 读取。变量只能通过值传递,因此不必担心在当前作用域之外写入。

let title = $state("title")

// `SomeComponent` can't change `title` that you see declared in this file
<SomeComponent title={title} />

// Now `SomeComponent` can update it
<SomeComponent title={title} updateTitle={(v) => title = v} />
Enter fullscreen mode Exit fullscreen mode

从机制上讲,这与编译后的 Solid 示例基本相同。在这两种情况下,信号都不会离开组件,并且唯一改变它的方式是与它的定义一起定义。


从上层接收值

图片描述

这种按值传递的方法对于传入组件的数据也非常有利。想象一下,如果你可以传递信号或值的话。

function SomeComponent(props) {
  createEffect(() => {
    // Do we call this as a function or not?
    document.title = props.title
  })
}
Enter fullscreen mode Exit fullscreen mode

我们总是可以检查:

document.title = isSignal(props.title) ? props.title() : props.title
Enter fullscreen mode Exit fullscreen mode

但想象一下,你编写的任何组件中,每个 prop 都要到处都这么做。SolidJS 甚至没有自带一个插件isSignal来阻止这种模式。

作为组件作者,你可以强制只使用 Signals,但这不符合人体工程学。Solid 使用函数,所以可能没什么大不了的,但想象一下,如果你使用 Vue 或 Preact Signals,就会出现这种情况.value。你肯定不想强迫人们这样做:

<SomeComponent title={{value: "static title"}} />

// or unnecessary signal
const title = useSignal("static title")
<SomeComponent title={title} />
Enter fullscreen mode Exit fullscreen mode

我在这里并非批评这些 API,而是强调维护 props 的按值传递 API 接口的重要性。出于人体工程学和性能方面的考虑,你肯定不希望用户过度包装。

解决这个问题的方法是为响应式和非响应式值提供相同的接口。然后,如果需要,就把所有 props 都当作响应式的。如果你把所有 props 都当作响应式的,你就不必担心上面发生的情况了。

对于 Solid 来说,反应性 props 是 getters:

<SomeComponent title={title()} />

// becomes
SomeComponent({
  get title() { return title() }
})

// whereas
<SomeComponent title="static title" />

// becomes
SomeComponent({
  title: "static title"
})

// Inside our component we aren't worried about what is passed to us
function SomeComponent(props) {
  createEffect(() => {
    document.title = props.title
  })
}
Enter fullscreen mode Exit fullscreen mode

使用 getters 还有一个额外的好处,即写回 props 不会更新值,从而强制进行变异控制。

props.title = "new value";

console.log(props.title === "new value"); //false
Enter fullscreen mode Exit fullscreen mode

思维局部性的局限性

图片描述

虽然局部性可能是现代 UI 实践最有价值的成果,但我们如今使用的工具并未完美地实现这种思维方式。UI 组件并非都是纯粹的。它们有状态。虽然不一定会产生外部副作用,但我们在闭包中保存的引用可能会影响未来的执行,这意味着这些执行确实很重要。

即使遵循这些原则,我们仍然无法控制父组件调用我们的频率。一方面,我们可以将组件视为输入的纯粹输出,这样可以简化操作。但有时,当我们遇到性能问题时,问题并非出在我们正在做的事情上,而是出在父组件之上,导致我们被迫脱离本地框架。

搭配一个不鼓励父级重复调用我们的模型,这通常大有裨益。这也是框架选择信号而非 VDOM 重新渲染的几个原因之一。信号并非能够完全避免来自父级的过度通知,而是由于保护机制的粒度更细,并且内置于模型中,其影响通常要小得多,而且发生的频率也更低。


总结

我曾与许多 React 的资深用户交流过,他们还记得这些细粒度的响应式模式何时走到了尽头。他们还记得像事件通知这样疯狂的蝴蝶效应循环。但如今的现实是,当我们审视 Signals 时,这些担忧已经荡然无存。

这更像是将事物分解为更易于控制的部分的演变。

组件 -> 钩子 -> 信号

但前提是我们必须遵循 React 最初制定的原则。Solid 没有isSignal或 Svelte Runes 不允许你将信号赋值给变量是有原因的。我们不想让你担心视图之外的数据图。

在你的本地作用域内,没有办法避免它。JavaScript 不会进行自动粒度更新,所以即使我们尝试用你能想到的最好的编译器(包括自动响应或记忆功能)来隐藏它,你也需要让语言能够理解你所看到的内容。

共同点在于,如果你把所有可能的东西都当成响应式的,那么决定什么是响应式的负担就会被推到消费者身上,无论你处理的是简单的信号、嵌套的存储、从 props 传递的原语还是来自全局单例。无论你对解决方案的依赖程度有多高。

消费者,国家的所有者(或者至少是国家的传承者),正是能够做出这一决定的人。如果你赋予他们本土化思考的能力,你就减轻了他们的负担,让他们有信心做出正确的决定。

鏂囩珷鏉yu簮锛�https://dev.to/this-is-learning/thinking-locally-with-signals-3b7h
PREV
既然 Bun 也是捆绑软件,为什么还要使用 Vite? - Vite 与 Bun 的对比
NEXT
重新审视 UI 组件的真正成本