本节摘要:React DevTools 回答"组件树长什么样、谁渲染了几次";solid-devtools 回答"依赖图长什么样、这次更新从哪个信号来"。工具的眼睛换了,排查思路也要换:本节导览三大面板,给出三类高频 bug 的定位手法,并说明服务端渲染场景的调试差异。
迁移后的第一周,团队一定会遇到同一类 bug:界面没更新,但改状态的代码明明执行了。React 时代的排查反射是看 DevTools 里组件有没有重渲染、props 对不对;这套反射在 Solid 里全部失效——组件没有重渲染,props 是取值器,肉眼盯组件树什么都看不出来。正确的问题是:这个节点有没有建立订阅?订阅的信号变了吗?回答它需要 solid-devtools——一套浏览器插件加调试器包,把依赖图直接画给你看。
组件面板。展示运行中的组件层级,点击定位到源码,查看每个组件创建的信号与 store 状态。与 React DevTools 的组件面板角色相同,是你的第一落点。
Owner 面板。展示作用域归属树(4.2 的概念落地成图):哪个 Effect 挂在哪个组件下、哪个控制流分支是谁的子作用域。排查"清理逻辑没执行""Effect 多跑"这类归属问题,看这棵树。
信号图面板。把信号、Memo、Effect 画成节点,订阅关系画成边,一次更新会在图上高亮传播路径。这是 React 侧没有对应物的独有视角——它让你"看见"第 4 章讲的更新投放。性能问题(依赖线过宽)与正确性问题(该有的边不存在)都能在图上直接读出。
第一类:界面没更新。步骤:打开信号图找到驱动该节点的信号,手动改值,看边会不会亮。边亮了节点没动——绑定目标错了(常见于手写 ref 操作);边根本不存在——订阅没建立,回头查三件事:是否解构了 props(3.3)、是否在非响应式上下文读取(回调里读信号不会登记)、列表是不是写在表达式里(4.3)。这套排查在 React 里等价于"看 props 变没变、memo 有没有挡",但 Solid 的答案永远落在"边"上。
第二类:Effect 执行次数不对。比预期多:在信号图上看它的入边,多出来的那条边就是意外的依赖——常见于在 Effect 里读了不该读的信号。比预期少:Effect 建在错误的作用域下(Owner 面板看归属),或者依赖信号被相等判断拦下(2.1 的 equals)。React 侧同类问题的排查靠 console.log 计数与依赖数组逐项删,Solid 的图直接给出因果。
第三类:内存缓慢上涨。页面切换后旧作用域没释放:Owner 面板里反复导航,看同名作用域是不是越堆越多。常见根因:在全局(无作用域)注册了订阅、第三方实例在 onCleanup 之外持有 DOM 引用。React 时代的对应物是"卸载了却没解绑监听",图谱化之后,泄漏从"猜"变成了"看"。
// 配合调试的开发期接入(示意):在应用入口挂上调试器 import { attachDevtoolsOverlay } from "solid-devtools/overlay"; attachDevtoolsOverlay({ name: "dev" });
| 需求 | React DevTools | solid-devtools |
|---|---|---|
| 看结构 | 组件树 | 组件树加 Owner 树 |
| 看更新 | Profiler 渲染计数与耗时 | 信号图传播高亮 |
| 找"没更新"的原因 | 检查 props 与 memo 拦截 | 查订阅边是否存在 |
| 找"更新太多" | 渲染火焰归因 | 入边过宽的节点 |
| 状态检视 | 组件 state 与 hooks 值 | 信号与 store 值 |
| 泄漏排查 | 卸载监听断点 | Owner 堆积观察 |
表的最后一行值得展开:Solid 的泄漏排查有结构性优势——作用域是显式的、图是可视的,"什么还活着"一目了然;React 的闭包与订阅散落在 Hook 链里,泄漏排查长期靠内存快照对比的笨办法。
💡 调试期的临时仪器只有一样是必备的:给可疑信号手动加一个打印 Effect(
createEffect(() => console.log(xxx())))。它打印的次数就是这个信号的真实订阅触发次数——比任何推测都可信。排查完删掉即可。
把第一类 bug 的手法走成完整记录。现象:详情页顶部的面包屑显示订单号,从列表页跳转到不同订单时,面包屑偶尔停留在上一个订单号上,而页面其余数据是新的。
排查第一刀:给面包屑的来源加打印 Effect,createEffect(() => console.log(params.orderId))。刷新跳转,控制台没有新输出——说明订阅没建立或读错了东西,问题定死在读取侧。第二刀:检查代码,发现面包屑组件是这样写的:
// 问题代码:解构丢掉了响应性 function Breadcrumb({ params }) { const { orderId } = params; return <span>订单 {orderId}</span>; }
又是解构——第 3 章的老朋友,这次藏在"从父组件传下来的 params"里,比直接解构 useParams() 的返回值更隐蔽。第三刀:修复,保持 props.params.orderId 的访问链或直接在组件内调用 useParams()。第四刀:回归信号图,改订单号时对应边正常点亮,验证通过。第五刀:归档——把"路由参数与 props 一样禁止解构"补进 8.2 清单。
这次复盘的元教训比 bug 本身值钱:解构断流不是某一个 API 的坑,而是"快照思维"的惯性残留——任何地方拿到一个 Solid 对象,先问它是活的还是快照。
分层看。服务端段的问题(数据函数报错、渲染异常)走 Node 侧的调试通道:日志加断点调试器,与调试任何服务端代码无异;留意服务端没有浏览器 API,报错里出现 window 未定义多半是组件里混入了仅客户端逻辑,需要环境判断包裹。客户端注水段的问题用 9.2 的三面板:注水后图已重建,调试体验与纯客户端一致。中间的衔接段(HTML 有内容但不可交互)先查控制台有无注水报错——多数是两端渲染结果不一致,常见根因是渲染过程中读取了随机值或时间。
下一节把应用送上生产——部署形态、缓存策略与监控落地的完整清单。