Svelte 5 深度调试指南:$inspect rune 的响应式日志、.with 回调与 $inspect.trace 原理剖析
Svelte 5 中的 $inspect rune 是官方提供的响应式调试工具:它像 console.log 一样打印值,但会在参数(包括深层嵌套状态)变化时自动重新执行;通过 .with 可以接管默认输出,通过 $inspect.trace 可以追踪某个 effect 或 derived 为何被触发。本文基于 Svelte 仓库官方文档([07-inspect.md?utm_source=gitcode_repo_files))并结合编译器与运行时的实际实现源码,完整讲解其用法、行为边界与底层原理,帮助你在开发环境中精确定位状态变化的来源。
注意:
$inspect仅在开发环境下生效。在生产构建(production build)中它会变成空操作(noop),不会留下任何运行时开销。这一点在编译器源码中可以得到直接印证:客户端转换阶段的 transform_inspect_rune 函数在!dev时直接返回b.empty,即该语句被整体消除,产物中根本不存在对应的运行时调用。
一、基础用法:自动重跑的 console.log
$inspect rune 大致等价于 console.log,区别在于:每当它的参数发生变化时它会重新执行。更关键的是,$inspect 对响应式状态做深度追踪——即使变化发生在对象或数组内部(通过细粒度响应式更新),它也会重新触发:
<!-- file: App.svelte -->
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // 当 `count` 或 `message` 变化时会 console.log
</script>
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
行为要点:
- 深度追踪:
$inspect({ list })这类写法能捕获list内部元素通过细粒度响应式发生的更新,而不仅仅是list引用本身被重新赋值。仓库中的测试样例(如 inspect-deep、inspect-deep-array)正是针对这种“深层变化也能触发”的场景做的验证。 - 更新时附带调用栈:状态变化导致的重新打印会输出 stack trace,方便找到状态变更的起点(playground 环境除外,受技术限制无法获取)。这个特性对应运行时 inspect.js 中
show_stack分支的逻辑:当不是首次(!initial)执行时,通过get_error('$inspect(...)')构造一个当前调用栈,并以折叠分组console.groupCollapsed('stack trace')的形式输出。
运行时实现:eager effect + snapshot
从 packages/svelte/src/internal/client/dev/inspect.js 的源码可以看出 $inspect 的完整工作方式:
eager_effect同步执行:源码注释明确说明 inspect effect 是同步运行的,目的是捕获有意义的堆栈信息。副作用是——读取值可能会报错,例如$inspect(object.property)会在包含它的{#if object}...{/if}之前运行,此时object可能尚未初始化。snapshot快照:每次取值后调用snapshot(value, true, true)对值做深克隆快照,再在untrack中执行 inspector。这样打印的值是“那一刻的快照”,且日志的打印过程不会意外建立新的响应式依赖。- 错误兜底:
eager_effect中若get_value()抛出异常,错误会被暂存;随后一个render_effect若正常运行则将错误通过console.error输出,若组件已销毁则不再打扰。对应测试样例 inspect-exception 验证了这种异常场景。
此外,validate_effect('$inspect') 会校验调用位置,$inspect 与 $effect 一样必须处于有效的 effect 上下文中,不能是孤儿 effect。
二、$inspect(...).with(...):自定义输出回调
$inspect(...) 返回一个带有 with 方法的对象。你可以传入一个回调,它会替代默认的 console.log 被调用。回调的第一个参数是 "init" 或 "update"(分别表示首次执行和后续更新),之后的参数就是你传给 $inspect 的各个值:
<!-- file: App.svelte -->
<script>
let count = $state(0);
$inspect(count).with((type, count) => {
if (type === 'update') {
debugger; // 或者 console.trace,或者你想做的任何事
}
});
</script>
<button onclick={() => count++}>Increment</button>
典型用法:只在“更新”时触发 debugger 断点,跳过初始化阶段的噪音;或把值收集到数组/面板中做自定义可视化。
从编译器角度,get_inspect_args 函数负责解析两种形态:
$inspect(...):inspector 固定为console.log;$inspect(...).with(fn):inspector 替换为你的回调fn。
两者最终都转换为对运行时 $.inspect(get_value, inspector, show_stack) 的调用;其中 show_stack 仅在纯 $inspect 形态下为 true,也就是只有默认 console.log 形态才会在更新时追加堆栈输出。CallExpression.js 中还有一处细节值得注意:传递 inspector 时包了一层箭头函数,这样日志的堆栈看起来来自 $inspect 调用处,而不是内部的 inspect.js 工具文件——这对“根据堆栈定位状态变化源头”非常有用。
三、$inspect.trace(...):追踪 effect 与 derived 的触发原因
$inspect.trace 是 Svelte 5.14 引入的 rune。它会让所在函数在开发模式下被“追踪”:每当该函数作为 effect 或 derived 的一部分重新运行时,控制台会打印出是哪些响应式状态导致了这次触发。
<script>
import { doSomeWork } from './elsewhere';
$effect(() => {
// $inspect.trace 必须是函数体的第一条语句
$inspect.trace();
doSomeWork();
});
</script>
$inspect.trace 接受一个可选的首个参数作为标签(label),用于在输出中识别该函数。
编译期约束:位置与语法校验
仓库在 analyze 阶段(2-analyze/visitors/CallExpression.js)对 $inspect.trace 有明确校验,对应 errors.js 中的两个错误码:
inspect_trace_invalid_placement:$inspect.trace(...)必须是函数体的第一条语句,且所在函数必须是函数声明、函数表达式或箭头函数;inspect_trace_generator:不能用在生成器函数(generator function)内;- 参数个数限制为“零或一个参数”,超出会报
rune_invalid_arguments_length错误。
如果没有显式传标签,分析阶段会自动生成一个标签:取函数自身的 label,并拼接源码位置 locate_node(fn) 得到形如 函数名 (位置) 的字符串。
运行时机制:编译器注入 $.trace
$inspect.trace 本身在产物中会被语句级移除(client/visitors/ExpressionStatement.js 中遇到该 rune 直接返回 b.empty),真正的追踪逻辑由编译器注入:
- 标记分析:analyze 阶段把 scope 的
tracing设置为标签的 thunk(() => 'label (位置)'),并将analysis.tracing置为true(见 2-analyze/visitors/CallExpression.js)。 - 注入 flag 导入:当
analysis.tracing为真时,transform-client.js 会为组件和模块各自注入import 'svelte/internal/flags/tracing',该文件仅执行 enable_tracing_mode_flag(),即开启运行时全局的追踪模式开关。 - 包装函数体:client/visitors/BlockStatement.js 中,若当前 scope 存在
tracing,则把函数体包装成对$.trace(标签, ...)的调用——运行时借此记录并输出“哪些被读取的 state 使该函数重跑”。
也就是说,$inspect.trace 是典型的“编译期标记 + 运行期插桩”设计:源码里只写一行 rune,产物里由编译器完成全部插桩,且仅在 dev 编译模式下生效。
四、适用边界与最佳实践
结合文档声明与源码证据,可以归纳出以下使用要点:
| 要点 | 说明 | 依据 |
|---|---|---|
| 仅开发环境生效 | production 构建中 $inspect / $inspect.trace 被整体编译消除,零开销 |
CallExpression.js 中 if (!dev) return b.empty |
| 深度响应式追踪 | 对象/数组内部细粒度更新也会触发重跑 | 文档原文 + inspect.js 的 snapshot 深克隆 |
| 同步执行,可能提前报错 | 读取值可能先于相关条件分支执行,异常由 render_effect 兜底打印 |
inspect.js 源码注释与错误处理逻辑 |
.with 回调首参为 "init" / "update" |
可区分初始化与后续更新,便于只在 update 时断点 | 文档 + utils.js 的 inspector 解析 |
$inspect.trace 必须是函数体首条语句 |
不能用于生成器函数,可选一个标签参数 | errors.js 两个专用错误码 |
| 堆栈输出 | 更新时会打印 stack trace,定位状态变化源头(playground 除外) | 文档原文 + show_stack 分支 |
实践建议:
- 调试“这个状态到底为什么变了”时,优先用
$inspect(x)观察值变化,再配合其自动输出的 stack trace 回溯赋值位置; - 需要断点而非打印时,用
$inspect(x).with((type, x) => { if (type === 'update') debugger; }),避免 init 阶段的误触发; - 调试“这个
$effect为什么重跑”时,在 effect 体首行加$inspect.trace(),控制台会列出触发重跑的响应式读取; - 由于生产构建会完全消除这些语句,放心把调试语句留在代码中提交,不会污染产物(这也是它与裸
console.log最大的工程价值差异)。
更多行为细节可参考仓库内的运行时测试样例:inspect、inspect-console-trace、inspect-derived、inspect-map-set 等,覆盖了基础、堆栈、derived 联动、Map/Set 状态等典型场景。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00