本节摘要:React 的生命周期围绕"反复执行"设计——挂载、更新、卸载三个阶段里,更新占了大头;Solid 组件只执行一次,生命周期退化为"装载"与"清理"两端。本节对位
onMount、onCleanup与 React 各生命周期钩子,用订阅报纸的隐喻重新解释"卸载",并给出 ref 回调与 DOM 测量的迁移写法。
在类组件时代,React 生命周期是一张著名的挂图:constructor、componentDidMount、componentDidUpdate、componentWillUnmount……每一个钩子都在回答"组件处于哪个阶段"。Hooks 出现后这张挂图折叠进了 useEffect,但折叠的方式是"用依赖数组区分挂载与更新",心智模型没变。Solid 的变化更彻底:组件函数执行完毕即退场,没有"更新阶段"这个概念,生命周期只剩两件事——DOM 就绪后做点什么(装载),作用域销毁前收尾(清理)。
先看最常见的需求——进页面拉数据、初始化第三方控件、测量 DOM 尺寸:
import { onMount } from "solid-js"; function Chart(props) { let canvasRef; onMount(() => { // 此刻 DOM 已挂载,可以安全测量与初始化 const chart = initChart(canvasRef, props.data); onCleanup(() => chart.destroy()); // 清理就近登记 }); return <canvas ref={canvasRef} />; }
对位关系一句话讲完:onMount 相当于 useEffect(fn, [])——只在首次渲染后跑一次,没有依赖数组,也没有"会不会因为漏写依赖多跑"的疑虑,因为它本来就只跑一次。它底层就是一个 Effect,只是语义上承诺"此刻 DOM 已就绪"。需要更早介入(渲染过程中、DOM 尚未提交)的场景,Solid 提供 createRenderEffect 等渲染期变体,日常业务用不到,第 4 章架构剖析时再碰。
React 的清理函数挂在 Effect 的返回值上;Solid 把它做成独立 API onCleanup,登记在当前作用域(Owner)上。这个"作用域"概念值得用一个隐喻讲透。
组件挂载相当于订了一份报纸:信号是报社,组件里的绑定是订户。onCleanup 登记的就是"退订动作"——组件销毁时,它名下所有订户自动退订,登记的收尾逻辑(解绑全局事件、清定时器、销毁第三方实例)逐个执行。关键差别在于登记的归属:React 的清理跟着"那一次 Effect"走,Solid 的清理跟着"当前作用域"走——写在组件顶层就随组件销毁执行,写在 createEffect 里就随该 Effect 每次重跑前执行,写在 Show 分支里就随分支切换执行。清理与作用域同生共死,不存在"忘了在卸载钩子里补一刀"的问题。
订阅全局事件的完整对位示例。React 版本:
useEffect(() => { const onKey = e => movePlayer(e.key); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []);
Solid 版本:
onMount(() => { const onKey = e => movePlayer(e.key); window.addEventListener("keydown", onKey); onCleanup(() => window.removeEventListener("keydown", onKey)); });
形状几乎一样,差别在底层保证:Solid 组件销毁时,所有以它为 Owner 的订阅(包括信号订阅、事件委托引用)都会被系统性地释放,onCleanup 只是这道保险上的人为补充。React 里"卸载"是一个要开发者主动配合的生命周期事件,Solid 里"卸载"是依赖图的拆图动作,退订是默认行为而不是约定。

React 心智里最重的"更新阶段",在 Solid 里没有一个对应物,因为更新发生在组件之外:信号、Memo、绑定构成的依赖图在组件退场后继续工作,DOM 节点持续被改写,但组件函数不会因此再执行一行代码。迁移者要做的不是"找到 componentDidUpdate 的替代品",而是把"检测到 props 变化就做事"的需求重新归类——绝大多数属于两类:要更新 DOM,写进 JSX 表达式即可;要执行副作用,放进 createEffect。真正需要"props 变化时手动响应"的场景,用 on(() => props.id, fn) 精确订阅。
💡 ref 回调在两个框架里都存在且用法相近,但 Solid 里它有个隐藏福利:组件只跑一次,ref 回调里的赋值语句只执行一次,把节点存进普通
let变量就是安全的——不需要useRef那层不可变包装。
用三个高频场景把生命周期对照落到手感。实例一,埋点上报——React 里常见 useEffect(() => { report("page_view"); }, []),Solid 里放 onMount,一字不差;如果上报发生在路由切换,改放路由层的钩子里(第 6 章),组件层不掺和。
实例二,第三方控件初始化——图表、编辑器、地图这类命令式库是清理逻辑的重灾区:
let editorRef; onMount(() => { const editor = createEditor(editorRef, props.config); onCleanup(() => { editor.destroy(); // 控件自带的销毁必须调用 editorRef.innerHTML = ""; // 控件留下的 DOM 残骸顺手清掉 }); }); <div ref={editorRef} />;
React 版本的等价代码结构相同,差别依旧在底层:Solid 组件销毁时,这段 onCleanup 一定执行,因为它登记在组件作用域上;React 版本依赖卸载流程正常走到 useEffect 的清理——绝大多数时候会,但错误边界跳过、StrictMode 双挂载等边角会让清理时序出现意外,迁移文档里值得标注这条差异。
实例三,离开确认(表单脏检查)——React 时代常用 useEffect 配合路由阻塞器;Solid 里直接在组件作用域登记 onCleanup,组件因路由切换销毁时触发确认逻辑,语义反而更贴合"离开这个页面"的本意。三个实例的共同启示:旧习惯里"找个钩子放代码"的思路,换成"这段代码的生死跟谁绑定"的思路,写法自然就对了一半。
组件自身的骨架清楚了,下一节解决复用问题——自定义 Hook 这套 React 生态的核心资产,如何改写成 Solid 的组合原语。