机制、触发场景与逐一排查指南)
Svelte 客户端运行时警告client-warnings机制、触发场景与逐一排查指南【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte本文以 Svelte 仓库中 client-warnings/warnings.md 为骨架系统讲解 Svelte 5 客户端运行时全部 20 个警告码的消息生成机制、触发原理与修复方式并结合packages/svelte/src/internal/client下的源码实现帮助读者在遇到[svelte] xxx控制台警告时能快速定位根因、消除告警。警告从哪来messages 目录与代码生成管线阅读每个警告之前先理解 Svelte 的警告体系是如何构建的——这决定了你在控制台看到的内容为什么是这个样子以及为什么部分警告可以静默。Svelte 采用“单一事实来源 代码生成”的管线维护运行时警告警告文案的唯一来源是分类目录下的 Markdown 文件。本文关注的客户端运行时警告位于 warnings.md每个警告对应一个## 警告码小节小节中引用块前缀是实际输出的警告消息允许一条引用块内写多个变体例如hydration_html_changed就带位置和不带位置两种文案引用块之外的正文是详细文档即“细节说明”构建脚本 scripts/process-messages/index.js 用正则## ([\w])\n\n([^]?)(?$|\n\n## )逐条解析这些小节引用块行被剥离前缀后作为messages数组其余段落拼成details重复警告码会直接抛错Duplicate message code该脚本一方面把细节文档生成到documentation/docs/98-reference/.generated/client-warnings.md另一方面生成运行时模块 src/internal/client/warnings.js——文件首行明确标注This file is generated by scripts/process-messages/index.js. Do not edit!其中每个导出函数与一个警告码同名。生成的警告函数有一个统一的行为约定以 warnings.js 中的await_waterfall为例export function await_waterfall(name, location) { if (DEV) { console.warn(%c[svelte] await_waterfall\n%cAn async derived, \${name}\ (${location}) was not read immediately after it resolved. ...); } else { console.warn(https://svelte.dev/e/await_waterfall); } }也就是说开发模式DEV输出带样式的前缀[svelte] 警告码 完整消息文案参数%name%、%location%等占位符在函数签名中体现如await_waterfall(name, location)生产模式只输出一条指向该警告文档的短地址svelte.dev/e/code形式提示开发者去查资料不占用控制台篇幅每个警告码全局唯一这是它能被svelte-ignore注释静默、并被文档系统检索的前提。此外部分警告码同时被编译器消费。以await_waterfall为例客户端转译阶段在 VariableDeclaration.js 中计算location时就会检查!is_ignored(init, await_waterfall)——即在源码对应位置写!-- svelte-ignore await_waterfall --注释即可连位置信息一起屏蔽。而hydration_attribute_changed、hydration_html_changed这类纯运行时比较产生的警告官方给出的静默手段同样是svelte-ignore注释。响应式状态类警告$state / $derived / asyncassignment_value_stale复合赋值读到的是“赋值前”的值Assignment to%property%property (%location%) will evaluate to the right-hand side, not the value of%property%following the assignment. This may result in unexpected behaviour.这是 Svelte 5 响应式模型下最隐蔽的坑。文档给出的典型场景script let object $state({ array: null }); function add() { (object.array ?? []).push(object.array.length); } /script button onclick{add}add/button pitems: {JSON.stringify(object.items)}/p第一次点击按钮时??的右侧[]被赋值给object.array但整个表达式object.array ?? []求值出来的仍是右侧那个[]紧接着.push(...)作用在这个局部数组上而object.array此时是一个空的状态代理push 进去的值随之丢失。源码机制dev 模式下对状态代理的复合赋值、、||、??会被编译器改写为assign(object, property, operator, rhs, location)调用实现见 dev/assign.jsexport function assign(object, property, operator, rhs, location) { return compare( operator ? (object[property] rhs) : ... , untrack(() object[property]), property, location ); }compare会把赋值后重新读取的属性值untrack包裹避免引入依赖与赋值表达式自身求值的结果比较当二者不同、且右值带有STATE_SYMBOL即是一个$state代理时触发警告。异步版本assign_async用同样的方式覆盖object.array ?? await ...这类写法。修复拆成两条语句让赋值先完成、再基于状态读取后续操作let object { array: [0] }; // ---cut--- function add() { object.array ?? []; object.array.push(object.array.length); }await_reactivity_lossawait 之后读取状态导致“反应性丢失”Detected reactivity loss when reading%name%. This happens when state is read in an async function after an earlierawaitSvelte 的信号机制在模板或$derived(...)执行时记录哪些状态被读取。如果表达式里带有await编译器会把await之后读取的状态也纳入依赖——也就是说let a Promise.resolve(1); let b 2; // ---cut--- let total $derived(await a b);这里a和b都会被跟踪尽管b是在a解析之后才被读取的。但如果await藏在另一个异步函数内部这种“可见性”就断了let a Promise.resolve(1); let b 2; // ---cut--- async function sum() { return await a b; } let total $derived(await sum());此时total只依赖立即被读取的a而不依赖b——b变化不会触发更新。文档给出的解法是把值作为参数传入让读取发生在当前作用域/** * param {Promisenumber} a * param {number} b */ async function sum(a, b) { return await a b; } let total $derived(await sum(a, b));源码机制这是纯 dev-only 检查。runtime.js 在读取任意状态信号时维护一个reactivity_loss_tracker当某次读取发生在异步派生“挂起后”且该信号从未被该派生登记为依赖、又不在当前 batch 处理中时调用w.await_reactivity_loss(signal.label)并额外打印一段“traced at”调用栈帮助定位是哪个异步边界吞掉了依赖。await_waterfall异步派生之间的无谓“瀑布”An async derived,%name%(%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app文档示例async function one() { return 1 } async function two() { return 2 } // ---cut--- let a $derived(await one()); let b $derived(await two());第二个$derived要等第一个解析后才被创建而await two()并不依赖a的值这段延迟俗称 waterfall是多余的。注意文档中的补充说明两个await的结果之后变化时是可以并发更新的瀑布只发生在派生首次创建时。修复方式是先创建 Promise、再等待它们let aPromise $derived(one()); let bPromise $derived(two()); let a $derived(await aPromise); let b $derived(await bPromise);源码机制deriveds.js 中异步派生解析且值发生变化时会被加入recent_async_deriveds集合并注册一个setTimeout0 宏任务到点时若该信号仍未被任何下游读取(effect.f DESTROYED) 0且仍在集合中判定为“解析了却没人消费”触发w.await_waterfall(signal.label, location)。位置信息由编译期提供见前文is_ignored检查因此该警告可以精确指向源码行。derived_inert读取属于已销毁 effect 的派生值Reading a derived belonging to a now-destroyed effect may result in stale values在$effect内部创建的$derived当该 effect 被销毁后就不再更新。正确做法是把$derived创建在 effect 之外或者放在$effect.root里。源码机制deriveds.js 的execute_derived开头检查派生的父 effect 是否带有DESTROYED | INERT标志若是则打印w.derived_inert()并直接返回缓存的旧值derived.v——这也解释了“stale values”的措辞运行时不抛错只是不再重算。console_log_stateconsole 打印了 $state 代理Yourconsole.%method%contained$stateproxies. Consider using$inspect(...)or$state.snapshot(...)instead浏览器 devtools 打印 Proxy 时展示的是代理本身而非其代表的值对 Svelte 而言$state代理的 target 可能看起来与当前值完全不同极易误导。源码机制dev 运行时把console.log等方法包装为 dev/console-log.js 的log_if_contains_state逐个检查参数是否带有STATE_SYMBOL若有则用snapshot(obj, true)来自 shared/clone.js做深快照先打印一行灰色的[snapshot]真实值再发出w.console_log_state(method)警告。修复想持续观察一个值随时间的变化用$inspect(...)rune见 documentation/docs/02-runes/07-$inspect.md想一次性打印当前值如事件处理器内用$state.snapshot(...)取快照见 documentation/docs/02-runes/02-$state.md。state_proxy_equality_mismatch代理与原值身份不同导致比较失真Reactive$state(...)proxies and the values they proxy have different identities. Because of this, comparisons with%operator%will produce unexpected results$state(value)返回的是value的 Proxy二者身份不同永远为falsescript let value { foo: bar }; let proxy $state(value); value proxy; // always false /script源码机制这是 dev 模式最“侵入”的一组检查见 dev/equality.js——init_array_prototype_warnings会临时打补丁替换Array.prototype的indexOf、lastIndexOf、includes当原方法返回“未找到”再逐元素用get_proxied_value(this[i]) item复核一遍一旦复核能命中说明你拿“代理数组里的元素”和“未代理的原始值”或反之做了比较于是以array.indexOf(...)等算子名触发警告。同样的复核逻辑也覆盖了/!//!strict_equals/equals两个包装函数equality.js。修复保持比较双方“同为$state创建”或“同为普通值”。另注意$state.raw(...)不创建状态代理可用于豁免不需要的深层代理。水合Hydration类警告SSR 客户端水合场景下Svelte 的策略是信任服务端 HTML对无法廉价修复的差异保留服务端值并报警告而不是悄悄修补。hydratable_missing_but_expectedExpected to find a hydratable with key%key%during hydration, but did not.如果客户端渲染了一个服务端没有渲染的hydratable水合时只能阻塞式地执行其异步函数块——这会卡住整个水合过程直到异步工作完成对性能很不利script import { hydratable } from svelte; if (BROWSER) { // bad! nothing can become interactive until this asynchronous work is done await hydratable(foo, get_slow_random_number); } /script源码机制hydratable.js 在水合阶段按 key 查找服务端预置节点查不到即调用w.hydratable_missing_but_expected(key)。hydration_attribute_changed属性值在服务端与客户端之间不一致The%attribute%attribute on%html%changed its value between server and client renders. The client value,%value%, will be ignored in favour of the server valueimg的src等属性在水合时不会被修复——因为更新它们可能触发图片重新请求iframe甚至整帧重载即使最终解析到同一资源。Svelte 因此保留服务端值。修复三选一用svelte-ignore注释 静默保证服务/客户端取值一致推荐若确实需要水合时变更按文档示例“暂存 → 置空 → 挂载后恢复”script let { src } $props(); if (typeof window ! undefined) { // stash the value... const initial src; // unset it... src undefined; $effect(() { // ...and reset after weve mounted src initial; }); } /script img {src} /源码机制dom/elements/attributes.js 在水合时比对 DOM 实际属性值与客户端计算值不一致即触发w.hydration_attribute_changed(attribute, html, value)。hydration_html_changed{html} 块内容不一致The value of an{html ...}block changed between server and client renders. The client value will be ignored in favour of the server value另一变体带%location%位置信息。{html ...}的值在服务端与客户端不一致时同样不会被修复因为水合期做 raw HTML 的差异检测代价高且通常没必要。修复思路与上一条完全一致静默、保证一致或按文档的“暂存-置空-$effect恢复”模板强制更新script let { markup } $props(); if (typeof window ! undefined) { const initial markup; markup undefined; $effect(() { markup initial; }); } /script {html markup}源码机制dom/blocks/html.js 调用w.hydration_html_changed(sanitize_location(location))同文件第 121 行在结构不匹配时还会落入w.hydration_mismatch()。hydration_mismatch水合失败初始 UI 与服务端渲染不符Hydration failed because the initial UI does not match what was rendered on the server.带位置变体... The error occurred near %location%水合时 Svelte 遍历 DOM 并预期遇到特定结构一旦实际结构不同——典型原因是浏览器按 HTML 规范自动“修复”了非法嵌套如div里放了p包div——遍历就会错位抛出此警告。文档特别提示开发模式下它前面通常会跟随一条console.error详细指出出问题的 HTML 片段需要优先修复该片段。源码机制调用点分布在 dom/hydration.js36/53/120 行节点/属性/文本三处校验失败、dom/blocks/html.js 与 render.js。绑定与属性所有权类警告binding_property_non_reactive%binding%is binding to a non-reactive property另一变体带(%location%)位置bind:的目标如果不在响应式系统管理范围内如普通模块常量、非状态对象绑定就无法回传更新。检查发生在 validate.js 的绑定校验逻辑中。ownership_invalid_binding跨层bind:缺少所有权声明%parent% passed property%prop%to %child% withbind:, but its parent component %owner% did not declare%prop%as a binding. Consider creating a binding between %owner% and %parent% (e.g.bind:%prop%{...}instead of%prop%{...})文档用三级组件说得很直白设GrandParent、Parent、Child三层。若你在GrandParent处写了GrandParent bind:value但GrandParent内部只通过Parent {value} /注意缺少bind:把值传下去Parent内部却写了Child bind:value——中间这一层没有声明绑定所有权警告即触发。修复在Parent上改为Parent bind:value /把绑定关系贯通到所有者。源码机制dev/ownership.js 在绑定建立时沿“属主链”核对每层是否以bind:声明了该属性。ownership_invalid_mutation修改未绑定的 propsMutating unbound props (%name%, at %location%) is strongly discouraged. Consider usingbind:%prop%{...}in %parent% (or using a callback) instead文档示例!--- file: App.svelte --- script import Child from ./Child.svelte; let person $state({ name: Florida, surname: Man }); /script Child {person} /!--- file: Child.svelte --- script let { person } $props(); /script input bind:value{person.name} input bind:value{person.surname}Child修改了属于App的person却没有被显式“授权”。这在大型项目里会让数据流难以推理“到底是谁改了这个值”因此被强烈不鼓励。修复改用回调 props 向上通信或者把person声明为$bindable让Child bind:person /成为合法的授权通道。源码机制dev/ownership.js 在检测到对无属主绑定属性的写入时调用w.ownership_invalid_mutation(name, location, prop, parent[FILENAME])。select_multiple_invalid_valueselect multiple的 value 必须是数组Thevalueproperty of aselect multipleelement should be an array, but it received a non-array value. The selection will be kept as is.使用select multiple value{...}时Svelte 通过遍历value数组来标记选中的option若传入非数组Svelte 发出此警告并保持当前选中状态不变。静默警告的前提是把value约束为两种合法形态显式选择传数组不改动选择传null或undefined。源码机制dom/elements/bindings/select.js 在同步多选项选中状态时发现类型不符即触发。组件生命周期、事件与其他警告lifecycle_double_unmount对未挂载组件执行 unmountTried to unmount a component that was not mounted典型的重复卸载/在错误的生命周期里调用$destroy类 API。调用点在 render.js属于防御性检查组件已经脱离渲染树后再收到一次卸载指令。svelte_boundary_reset_noopsvelte:boundary的 reset 只有一次效力Asvelte:boundaryresetfunction only resets the boundary the first time it is called当svelte:boundary内容渲染出错时onerror处理器会收到错误和一个reset函数用于触发内容重渲染。但这个函数只能有效一次。文档示例展示了反模式——把reset存到边界外部的引用里之后反复调用是无效的按钮点击不会再次渲染内容script let reset; /script button onclick{reset}reset/button svelte:boundary onerror{(e, r) (reset r)} !-- contents -- {#snippet failed(e)} poops! {e.message}/p {/snippet} /svelte:boundary修复思路每次onerror回调都把最新的一次性reset写入本地状态UI 中的恢复按钮直接闭包当前这次回调收到的reset而不是跨错误复用旧引用。源码机制dom/blocks/boundary.js 在检测到对已消费reset的二次调用时发出警告。invalid_raw_snippet_renderTherenderfunction passed tocreateRawSnippetshould return HTML for a single element面向需要把任意 HTML 包装成 snippet 的高级用法raw snippet渲染函数必须返回单个元素的 HTML。触发点见 dom/blocks/snippet.js。event_handler_invalid事件处理器不是函数%handler% should be a function. Did you mean to %suggestion%?给on:event传了非函数值常见于把方法名写成字符串、或误传属性值。警告会附带一条“你是不是想……”的建议。触发点见 dom/elements/events.js。legacy_recursive_reactive_block迁移自 Svelte 4 的递归反应块Detected a migrated$:reactive block in%filename%that both accesses and updates the same reactive value. This may cause recursive updates when converted to an$effect.Svelte 5 提供把旧版$:反应式语句自动迁移为$effect的 legacy 支持legacy-client.js。但旧式a a 1;这类“自读自写”语句在$effect语义下依赖 effect 的变更检测可能形成递归更新因此 legacy 层专门检测并预警。如果你正在做 v4 → v5 迁移参见 documentation/docs/07-misc/07-v5-migration-guide.md 与 99-legacy 文档目录见到此警告应把该语句显式改写成先读后写或独立状态。transition_slide_displayslide 过渡与 display 值不兼容Theslidetransition does not work correctly for elements withdisplay: %value%slide过渡通过动画元素的height实现因此要求元素具备可测量的盒模型。以下display值下它无法正常工作display: inlinespan等的默认值及其变体inline-block、inline-flex、inline-griddisplay: table与table-[name]table、tr的默认值display: contents。修复为承载元素加上display: block/flex/grid或换用fade、scale等不依赖盒模型的过渡。触发点见 transition/index.js——slide定义内部读取style.display并直接调用w.transition_slide_display(style.display)。二十个警告码速查表警告码一句话含义主要源码位置assignment_value_stale复合赋值表达式求值为右值后续操作可能作用在丢失的临时值上dev/assign.jsawait_reactivity_lossawait后在函数边界内读状态依赖未被跟踪runtime.jsawait_waterfall异步派生解析后未被及时读取存在无谓串行deriveds.jsbinding_property_non_reactivebind:目标不受响应式系统管理validate.jsconsole_log_stateconsole 直接打印了$state代理dev/console-log.jsderived_inert读取属于已销毁 effect 的$derived值已过期deriveds.jsevent_handler_invalid事件处理器不是函数dom/elements/events.jshydratable_missing_but_expected客户端渲染了服务端没有的 hydratable水合被阻塞hydratable.jshydration_attribute_changed关键属性如src在两端不一致保留服务端值dom/elements/attributes.jshydration_html_changed{html}内容两端不一致保留服务端值dom/blocks/html.jshydration_mismatch初始 DOM 结构与 SSR 输出不符水合失败dom/hydration.jsinvalid_raw_snippet_renderraw snippet 的 render 函数必须返回单元素 HTMLdom/blocks/snippet.jslegacy_recursive_reactive_block迁移的$:块自读自写$effect化后可能递归legacy-client.jslifecycle_double_unmount对未挂载组件执行了 unmountrender.jsownership_invalid_binding跨层bind:缺少中间层所有权声明dev/ownership.jsownership_invalid_mutation修改了未用bind:/回调授权的父级状态dev/ownership.jsselect_multiple_invalid_value多选value收到非数组保持原选中dom/elements/bindings/select.jsstate_proxy_equality_mismatch代理与原值身份不同/includes等结果失真dev/equality.jssvelte_boundary_reset_noopboundary 的reset第二次起不再生效dom/blocks/boundary.jstransition_slide_displayslide过渡在不支持高度动画的 display 下失效transition/index.js小结Svelte 的客户端警告体系有两个鲜明特点一是文案、文档、运行时函数三者同源——warnings.md 是唯一事实来源process-messages 脚本 生成 warnings.js 与参考文档警告码全局唯一且可被svelte-ignore静默二是重 dev、轻 prod——大部分检查反应性丢失追踪、原型打补丁、归属权验证、快照打印只在 DEV 下生效生产构建至多输出一条文档短地址。理解了这条管线与每个警告的触发点控制台里的[svelte] xxx就不再是噪音而是指向具体源码行为的精确诊断信息。【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考