本节摘要:
useEffect的心智负担集中在"手动声明依赖",createEffect把这件事交给运行时:函数体里读了哪些信号,依赖就是什么。本节先复盘依赖数组的三类经典坑,再并排改写成 Solid 版本,对位onCleanup与on,最后给出两框架在执行时序上的对照表。
为什么 Solid 的 Effect 不需要依赖数组?回答这个问题之前,先想清楚依赖数组到底在解决什么矛盾。React 的组件每次重渲染都会重新创建 Effect 函数,框架面对的是"一个全新的函数",它无从知道这个函数关心哪些值,只能请开发者白纸黑字写进数组。依赖数组不是 Feature,是重渲染模型的补丁——补丁本身又制造了新的问题面。
第一类是漏依赖:数组里少写了某个引用值,Effect 闭包捕获的是旧值,界面与数据悄悄分叉,这类 bug 的隐蔽性在于它不报错。第二类是多依赖:把对象或函数写进数组,它们每次渲染都是新引用,Effect 反复触发,工程师被迫用 useMemo 包一层来"稳住引用",代码开始为框架服务而不是为业务服务。第三类是清理遗漏:订阅了事件、启动了定时器,返回的清理函数忘了写或写得不全,组件卸载后留下悬空引用。
这三类坑的根源相同:依赖关系存在于代码的控制流里,却要靠人肉誊写进一个数组。Solid 的做法是让运行时在 Effect 函数首次执行时观察"它到底读了哪些信号",读到的就是依赖——控制流走到哪条分支,哪个分支里的信号才被登记,条件变化后依赖还会随之增删。依赖数组所承载的信息,被读取现场自然承载了。
需求:输入框内容变化后防抖发起搜索,组件卸载时要清理定时器并中断过期结果。React 版本:
import { useState, useEffect } from "react"; function Search() { const [query, setQuery] = useState(""); useEffect(() => { const timer = setTimeout(async () => { const res = await fetchResults(query); setResults(res); }, 300); return () => clearTimeout(timer); // 清理函数,别忘了依赖数组里的 query }, [query]); return <input value={query} onChange={e => setQuery(e.target.value)} />; }
Solid 版本:
import { createSignal, createEffect, onCleanup } from "solid-js"; function Search() { const [query, setQuery] = createSignal(""); createEffect(() => { const q = query(); // 读取现场 = 依赖登记 const timer = setTimeout(async () => { const res = await fetchResults(q); setResults(res); }, 300); onCleanup(() => clearTimeout(timer)); // 每次重新执行前,上一轮的清理先跑 }); return <input value={query()} onInput={e => setQuery(e.currentTarget.value)} />; }
两份代码结构相似,机制不同处值得逐条标注:其一,Solid 没有 [query],因为 query() 的调用已经被观察到;其二,onCleanup 的登记是"每轮一份",重新执行时上一轮清理先行,语义与 React 的清理函数一致,但不会因为"忘了写依赖"而整段失效;其三,注意 q 是在异步回调外取出的同步值——异步世界里没有自动追踪,createEffect 只追踪同步执行段读取的信号,这一点与直觉一致,却是迁移时的高频误区。

| 维度 | useEffect | createEffect |
|---|---|---|
| 首次执行 | 绘制完成后异步执行 | 首次渲染后执行(可配合渲染期变体提前) |
| 更新触发 | 依赖数组比对通过后 | 订阅的信号变化后,批量调度 |
| 清理时机 | 下次执行前或卸载时 | 下次执行前或 Owner 销毁时 |
| 显式指定来源 | 无此机制(靠数组) | on(deps, fn) 只盯指定信号 |
| 暂时屏蔽追踪 | 无此需求(数组即白名单) | untrack(fn) 读取不登记 |
表里 on 值得单独说。从 React 过来的开发者有时会怀念"我就是要显式列出依赖"的确定性,Solid 保留了这条路:on([a, b], () => {...}) 表示只有 a、b 变化才重新执行,函数体里读到的其他信号一律不算依赖。它是自动追踪的补充而非替代——默认信自动,需要开关阀门时用 on。
untrack 则是反向阀门:某段读取你明确不想建立依赖(比如只在调试时打印状态),包一层即可。React 没有对位物,因为它的模型里"读取"从不建立依赖。
💡 关键直觉:从 useEffect 迁移时,先删依赖数组,再逐行检查异步回调——把异步段要用的信号值在同步段先用变量接住,其余几乎可以原样平移。坑通常只剩这一个。
状态源与副作用都齐了,下一节处理两者的中间地带——嵌套对象与列表这类复合状态,看 createStore 怎么对位 React 的不可变更新。