2.2 createEffect与useEffect:依赖追踪的自动与手动


2.2 createEffect 与 useEffect:依赖追踪的自动与手动

本节摘要useEffect 的心智负担集中在"手动声明依赖",createEffect 把这件事交给运行时:函数体里读了哪些信号,依赖就是什么。本节先复盘依赖数组的三类经典坑,再并排改写成 Solid 版本,对位 onCleanupon,最后给出两框架在执行时序上的对照表。

为什么 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], () => {...}) 表示只有 ab 变化才重新执行,函数体里读到的其他信号一律不算依赖。它是自动追踪的补充而非替代——默认信自动,需要开关阀门时用 on

untrack 则是反向阀门:某段读取你明确不想建立依赖(比如只在调试时打印状态),包一层即可。React 没有对位物,因为它的模型里"读取"从不建立依赖。

💡 关键直觉:从 useEffect 迁移时,先删依赖数组,再逐行检查异步回调——把异步段要用的信号值在同步段先用变量接住,其余几乎可以原样平移。坑通常只剩这一个。

本节要点回顾

  • 依赖数组是重渲染模型的补丁:自动追踪不是省事,是把誊写副本换成读取现场的真相。
  • onCleanup 对位清理函数,每轮一份,重跑前先执行,卸载时由 Owner 兜底。
  • 追踪只覆盖同步段:异步回调里的信号要在同步段先取出。
  • on 与 untrack 是两个手动阀门:一个收紧依赖,一个豁免追踪。

状态源与副作用都齐了,下一节处理两者的中间地带——嵌套对象与列表这类复合状态,看 createStore 怎么对位 React 的不可变更新。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U