SolidJS 响应式系统的工作原理是什么?
SolidJS 的响应式系统是其区别于 React、Vue 等框架的核心设计,它放弃了虚拟 DOM,转而采用细粒度响应式 + 直接 DOM 更新的方式。理解这套机制,是掌握 SolidJS 的关键。
响应式原语:Signal、Effect、Memo、Resource
createSignal — 响应式状态的基本单元
createSignal 创建一个响应式状态,返回一个 getter 函数和 setter 函数:
javascriptconst [count, setCount] = createSignal(0); // 读取:调用 getter console.log(count()); // 0 // 写入:调用 setter setCount(1); setCount(prev => prev + 1); // 支持函数式更新
关键点:getter 是一个函数而非变量引用。这是依赖追踪的前提——只有执行 count() 时,SolidJS 才能知道当前上下文依赖了这个 signal。setter 触发后,不会重跑整个组件,只会精确更新依赖 count() 的那些 DOM 节点或计算。
createSignal 还接受第二个参数用于配置,例如自定义相等性判断:
javascriptconst [value, setValue] = createSignal(0, { equals: (newVal, oldVal) => newVal === oldVal });
当 equals 返回 true 时,setter 不会触发订阅者更新,这是避免无效渲染的一道防线。
createEffect — 自动追踪依赖的副作用
createEffect 在执行时自动收集内部读取的所有 signal,当这些 signal 变化时重新执行:
javascriptconst [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,只在依赖变化时重新计算:
javascriptconst [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 专门处理异步数据获取,返回一个响应式的信号对象:
javascriptconst [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 实现深层响应式:
javascriptconst [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 会被自动批处理:
javascriptconst [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 函数手动批处理:
javascriptimport { batch } from "solid-js"; setTimeout(() => { batch(() => { setX(1); setY(2); }); }, 1000);
onCleanup — 清理副作用
createEffect 中产生的副作用(定时器、事件监听、订阅)需要在 effect 重新执行或组件销毁时清理。onCleanup 注册清理回调:
javascriptcreateEffect(() => { const timer = setInterval(() => console.log(count()), 1000); onCleanup(() => clearInterval(timer)); });
onCleanup 在 effect 重新执行前和所属组件销毁时自动调用,是防止内存泄漏的关键机制。
与 React 的核心差异
理解 SolidJS 响应式系统的最好方式是与 React 对比:
| 维度 | React | SolidJS |
|---|---|---|
| 更新粒度 | 组件级重渲染 | 表达式级精确更新 |
| 依赖声明 | 手动 useEffect 依赖数组 | 运行时自动追踪 |
| DOM 策略 | 虚拟 DOM diff + 批量更新 | 直接 DOM 操作 |
| 组件行为 | 函数每次渲染重新执行 | 函数只执行一次 |
| 状态原语 | useState 返回值引用 | createSignal 返回 getter 函数 |
| 异步处理 | 手动管理 loading/error | createResource 内置状态 |
React 的组件函数在每次 state 变化时重新执行,内部的变量、函数都会重新创建。SolidJS 的组件函数只在挂载时执行一次,后续更新全部由响应式系统驱动,不需要 re-render 这个概念。
总结
SolidJS 响应式系统的核心思路是:通过 getter 函数调用实现运行时依赖收集,通过发布-订阅机制实现精确触发,通过调度队列实现批量有序更新。这套机制让 SolidJS 在保持声明式编程体验的同时,获得了接近原生 DOM 操作的性能。
掌握 createSignal、createEffect、createMemo、createStore 这四个原语及其背后的依赖追踪机制,是理解 SolidJS 的基础,也是前端面试中的高频考点。