5月27日 15:53

SolidJS 响应式系统的工作原理是什么?

SolidJS 的响应式系统是其区别于 React、Vue 等框架的核心设计,它放弃了虚拟 DOM,转而采用细粒度响应式 + 直接 DOM 更新的方式。理解这套机制,是掌握 SolidJS 的关键。

响应式原语:Signal、Effect、Memo、Resource

createSignal — 响应式状态的基本单元

createSignal 创建一个响应式状态,返回一个 getter 函数和 setter 函数:

javascript
const [count, setCount] = createSignal(0); // 读取:调用 getter console.log(count()); // 0 // 写入:调用 setter setCount(1); setCount(prev => prev + 1); // 支持函数式更新

关键点:getter 是一个函数而非变量引用。这是依赖追踪的前提——只有执行 count() 时,SolidJS 才能知道当前上下文依赖了这个 signal。setter 触发后,不会重跑整个组件,只会精确更新依赖 count() 的那些 DOM 节点或计算。

createSignal 还接受第二个参数用于配置,例如自定义相等性判断:

javascript
const [value, setValue] = createSignal(0, { equals: (newVal, oldVal) => newVal === oldVal });

当 equals 返回 true 时,setter 不会触发订阅者更新,这是避免无效渲染的一道防线。

createEffect — 自动追踪依赖的副作用

createEffect 在执行时自动收集内部读取的所有 signal,当这些 signal 变化时重新执行:

javascript
const [name, setName] = createSignal("Alice"); createEffect(() => { console.log(`Hello, ${name()}`); // 首次执行输出 "Hello, Alice" // setName("Bob") 后自动再次执行,输出 "Hello, Bob" });

与 React 的 useEffect 不同,这里不需要手动声明依赖数组。SolidJS 在运行时通过执行 getter 函数自动收集依赖,既减少了手写依赖的负担,也避免了依赖遗漏导致的 bug。

注意事项:

  • createEffect 在 DOM 更新之后、浏览器绘制之前执行,适合同步外部系统(日志、DOM 测量等)
  • 不要在 effect 中设置 signal 来派生状态,应使用 createMemo
  • effect 内条件性地读取 signal 会导致依赖不固定,每次执行收集到的依赖可能不同

createMemo — 缓存派生计算

createMemo 创建一个只读的派生 signal,只在依赖变化时重新计算:

javascript
const [count, setCount] = createSignal(0); const doubled = createMemo(() => count() * 2); console.log(doubled()); // 0 setCount(3); console.log(doubled()); // 6 — 自动重新计算

createMemo 的价值在于避免重复计算。当多个 effect 或 DOM 节点依赖同一个派生值时,memo 保证计算只执行一次,结果被缓存和共享。

createResource — 异步数据加载

createResource 专门处理异步数据获取,返回一个响应式的信号对象:

javascript
const [user] = createResource(userId, async (id) => { const res = await fetch(`/api/users/${id}`); return res.json(); }); // 模板中可直接使用 <div>{user.loading ? "加载中..." : user()?.name}</div>

createResource 内置了 loading、error 状态管理,比手动用 createSignal + createEffect 管理异步更可靠。

依赖追踪的底层机制

SolidJS 的依赖追踪基于发布-订阅模式,核心流程分三步:

第一步:收集。 当 createEffect 或 createMemo 执行时,SolidJS 将当前计算上下文压入一个全局栈。此时任何 signal 的 getter 被调用,都会将当前上下文注册为该 signal 的订阅者。

第二步:触发。 当 signal 的 setter 被调用且值确实发生了变化(通过 equals 判断),signal 通知所有订阅者。

第三步:调度。 被通知的计算不会立即同步执行,而是被放入一个调度队列。SolidJS 会批量处理同一事件循环中的多个更新,然后按依赖顺序依次执行,避免重复计算。

这种设计保证了:

  • 更新是批量且有序的,不会出现中间态
  • 一个 signal 变化不会导致无关的 effect 执行
  • 依赖关系在运行时动态建立,条件分支中读取的 signal 会随执行路径变化

createStore — 深层响应式对象

createSignal 适合原始值和浅层状态,但面对嵌套对象时逐一创建 signal 不现实。createStore 通过 Proxy 实现深层响应式:

javascript
const [state, setState] = createStore({ user: { name: "Alice", address: { city: "Beijing" } } }); // 读取深层属性自动追踪 createEffect(() => { console.log(state.user.address.city); }); // 精确更新,只触发依赖 city 的 effect setState("user", "address", "city", "Shanghai");

createStore 的关键特性是"按需追踪"——只有实际被读取的嵌套属性才会建立响应式关系。修改 state.user.address.city 不会触发只依赖 state.user.name 的 effect,这是细粒度响应式在对象层面的体现。

批量更新与事务

默认情况下,同一事件处理函数中的多个 setter 会被自动批处理:

javascript
const [x, setX] = createSignal(0); const [y, setY] = createSignal(0); createEffect(() => { console.log(`x=${x()}, y=${y()}`); }); // 批量更新,effect 只执行一次 setX(1); setY(2); // 输出 "x=1, y=2",而不是先输出 "x=1, y=0" 再输出 "x=1, y=2"

在异步回调中,可以使用 batch 函数手动批处理:

javascript
import { batch } from "solid-js"; setTimeout(() => { batch(() => { setX(1); setY(2); }); }, 1000);

onCleanup — 清理副作用

createEffect 中产生的副作用(定时器、事件监听、订阅)需要在 effect 重新执行或组件销毁时清理。onCleanup 注册清理回调:

javascript
createEffect(() => { const timer = setInterval(() => console.log(count()), 1000); onCleanup(() => clearInterval(timer)); });

onCleanup 在 effect 重新执行前和所属组件销毁时自动调用,是防止内存泄漏的关键机制。

与 React 的核心差异

理解 SolidJS 响应式系统的最好方式是与 React 对比:

维度ReactSolidJS
更新粒度组件级重渲染表达式级精确更新
依赖声明手动 useEffect 依赖数组运行时自动追踪
DOM 策略虚拟 DOM diff + 批量更新直接 DOM 操作
组件行为函数每次渲染重新执行函数只执行一次
状态原语useState 返回值引用createSignal 返回 getter 函数
异步处理手动管理 loading/errorcreateResource 内置状态

React 的组件函数在每次 state 变化时重新执行,内部的变量、函数都会重新创建。SolidJS 的组件函数只在挂载时执行一次,后续更新全部由响应式系统驱动,不需要 re-render 这个概念。

总结

SolidJS 响应式系统的核心思路是:通过 getter 函数调用实现运行时依赖收集,通过发布-订阅机制实现精确触发,通过调度队列实现批量有序更新。这套机制让 SolidJS 在保持声明式编程体验的同时,获得了接近原生 DOM 操作的性能。

掌握 createSignal、createEffect、createMemo、createStore 这四个原语及其背后的依赖追踪机制,是理解 SolidJS 的基础,也是前端面试中的高频考点。

标签:SolidJS