
Svelte $derived 完整指南派生状态的用法、依赖追踪与源码实现解析【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte本篇技术指南围绕 Svelte 官方文档中的$derivedrune 展开系统讲解派生状态的声明方式$derived与$derived.by、依赖追踪规则、值覆盖技巧乐观 UI、解构行为与 push-pull 更新传播机制并结合 Svelte 5 源码编译器转换层与客户端响应式内核说明每个行为的底层实现依据帮助读者在组件开发中正确、高效地使用派生状态。基本用法声明派生状态派生状态Derived state通过$derivedrune 声明script let count $state(0); let doubled $derived(count * 2); /button button onclick{() count} {doubled} /button p{count} doubled is {doubled}/p核心要点$derived(...)内部的表达式应当没有副作用。Svelte 禁止在派生表达式内修改状态例如不允许写count否则会破坏依赖追踪的确定性。与$state一样类字段class field也可以标记为$derived。组件中的代码只在创建时执行一次——如果不加$deriveddoubled将永远停留在初始计算出的值上count变化后也不会更新。这正是派生状态存在的意义把一个普通表达式变成“随依赖自动重算”的响应式值。注意上例中script块结束标签请按标准写法闭合为/script此处仅保留官方文档示例的逻辑主体。$derived.by多语句派生当派生逻辑过于复杂、无法塞进一个简短表达式时使用$derived.by它接受一个函数作为参数script let numbers $state([1, 2, 3]); let total $derived.by(() { let total 0; for (const n of numbers) { total n; } return total; }); /script button onclick{() numbers.push(numbers.length 1)} {numbers.join( )} {total} /button本质上$derived(expression)等价于$derived.by(() expression)。这一点在编译器中可以得到直接印证。在客户端转换层 CallExpression.js 中case $derived: case $derived.by: { let fn /** type {Expression} */ (context.visit(node.arguments[0])); return b.call($.derived, rune $derived ? b.thunk(fn) : fn); }即$derived(expr)会被包一层 thunk() expr后调用运行时的$.derived而$derived.by(fn)直接透传函数——两者最终都收敛到同一个derived信号构造函数仅参数形态不同。理解依赖追踪$derived表达式或$derived.by函数体中被同步读取的任何值都会被视为该派生状态的依赖。当依赖发生变化时派生值会被标记为dirty脏并在下次被读取时重新计算。含有await的表达式如果表达式中包含awaitSvelte 会对其进行特殊转换使得await之后的状态读取也会被追踪。例如let a Promise.resolve(1); let b 2; // ---cut--- let total $derived(await a b);此时a和b都会成为依赖即使b是在aresolve、首次执行之后才被读取的。这一规则不适用于表达式所调用的函数内部的await只针对表达式本身。这种语法糖背后是独立的async derived机制分析阶段会把含await的$derived记录为异步派生转换阶段VariableDeclaration.js会将其改写为await $.async_derived(...)形式运行时实现位于 deriveds.js 的 async_derived。从源码注释可以看到Svelte 刻意保证fn()的 thenable 属性在求值时被访问从而把 Promise 链中读取到的信号也纳入依赖追踪dev 模式下还配有reactivity_loss_tracker来检测跨await的响应性丢失。用 untrack 排除依赖如果希望某块状态不被当作依赖处理可以使用untrack。覆盖派生值乐观 UI派生表达式会在依赖变化时重新计算但你可以通过重新赋值来临时覆盖它的值除非用const声明。这在乐观 UIoptimistic UI场景非常有用值从“事实来源”例如服务端数据派生但需要立即向用户反馈操作结果script let { post, like } $props(); let likes $derived(post.likes); async function onclick() { // increment the likes count immediately... likes 1; // and tell the server, which will eventually update post try { await like(); } catch { // failed! roll back the change likes - 1; } } /script button {onclick} {likes}/button注意Svelte 5.25 之前派生值是只读的read-only。从编译器角度看对派生绑定binding.kind derived的重新赋值会走transform.assign通道并在 AssignmentExpression.js 中被显式排除在“代理化”proxy wrapping之外——也就是说直接给likes赋值不会尝试把新值转成响应式代理这正是“临时覆盖缓存值”这一语义在编译期的体现。派生状态与深度响应式与$state会把对象和数组转换为深度响应式代理 不同$derived的值保持原样as-is。例如考虑这样一个场景// errors: 7005 let items $state([ /*...*/ ]); let index $state(0); let selected $derived(items[index]);你可以修改或对selected使用bind:绑定它的属性这些修改会影响底层的items数组——因为selected指向的是$state创建的深度响应式代理中的元素。反之如果items本身不是深度响应式的修改selected将不会有任何效果。这提示了一个实践要点派生状态不创造新的响应式对象它只是响应式读取的缓存结果响应式的深度取决于源状态本身。解构派生值对$derived声明使用解构时得到的每个变量都是响应式的。这一写法function stuff() { return { a: 1, b: 2, c: 3 } } // ---cut--- let { a, b, c } $derived(stuff());大致等价于function stuff() { return { a: 1, b: 2, c: 3 } } // ---cut--- let _stuff $derived(stuff()); let a $derived(_stuff.a); let b $derived(_stuff.b); let c $derived(_stuff.c);编译器转换层完整实现了这一展开逻辑VariableDeclaration.js 中当声明目标不是简单标识符而是对象/数组模式时会先创建一个持有整个表达式的$$d派生信号再为解构路径上的每个属性分别生成一个$.derived(...)信号——与上述“等价写法”一一对应。因此对解构出的字段单独追踪变化是可行的而不是每次都整体重取。更新传播push-pull 响应式Svelte 使用所谓push-pull 响应式状态更新时所有直接或间接依赖它的节点会立即被通知变化“推”push但派生值本身要等到真正被读取时才重新求值“拉”pull。另一个关键优化是如果派生值的新值与旧值引用相同referentially identical下游更新会被跳过。例如script let count $state(0); let large $derived(count 10); /script button onclick{() count} {large} /button即使large依赖countSvelte 也只在large本身真正变化如从false变到true时才更新按钮内的文本count每次自增都不会触发 DOM 更新。这段行为在运行时源码中有清晰的对应实现。deriveds.js 中的derived(fn)创建信号时初始标记为DERIVED | DIRTY初始即为脏等待首次读取时计算并携带一个equals比较函数而 update_derived 在重算后首先执行var value execute_derived(derived); if (!derived.equals(value)) { derived.wv increment_write_version(); // ... derived.v value; // 或写入当前 batch }只有当新值与缓存值比较不相等时才会更新写版本write version并把新值写入信号——否则下游的反应reactions不会收到更新DOM 也就保持不变。这正是文档中“引用相同则跳过下游更新”的实现机制。运行时的入口与 dev 模式细节用户代码中的$derived最终调用的是经 internal/client/index.js 导出的user_derived别名derived。它在 deriveds.js 中实现export function user_derived(fn) { const d derived(fn); if (!async_mode_flag) push_reaction_value(d); return d; }其中push_reaction_value(d)把派生信号推入当前批次batch的追踪栈使依赖变化通知能够正确归入正在处理的更新批次。此外还有两点值得了解dev 模式下编译器会在$.derived(...)外再包一层$.tag(call, 变量名)见 VariableDeclaration.js为$inspect、报错定位等调试工具提供信号标签。execute_derived 在 dev 模式下维护一个调用栈若检测到派生表达式引用自身会抛出明确的错误derived_references_self避免直接爆栈式的 “too much recursion”。小结与源码索引$derived的设计可以概括为四句话无副作用的表达式声明、同步读取即依赖、脏标记加懒求值的 push-pull 传播、以及基于equals比较的下游更新剪枝。核心源码位置如下便于进一步深入关注点源码位置派生信号创建与重算deriveds.jsderivedL72、execute_derivedL340、update_derivedL393异步含await派生async_derived编译期 rune 转换CallExpression.js、VariableDeclaration.js派生值重新赋值AssignmentExpression.js配套文档参见 $state、await 表达式 与 svelte 模块参考untrack。【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考