用户报告踩坑 3 后,对照同类型框架(SolidJS / Vue / Angular / Svelte 5 / Preact)补齐。
## on(deps, fn) —— 显式声明 effect 依赖(对齐 SolidJS)
effect 的依赖是自动收集的,而「effect 只调了个函数」不等于没依赖:被调函数在
**首个 await 之前**的代码同步执行,所以它读/写的信号全算在 effect 头上。
effect(() => { if (session.value) void sync() })
// sync() 首个 await 前:读 entSync.value.syncing(守卫)+ 写 entSync.set(...)
// → effect 订阅了 ent.sync → 写它又触发自己 → 无限重同步(C210 + C211)
对策此前只有 untrack(能用,但靠人记得)。on() 让回调跑在 untrack 作用域里,
于是它调用的函数碰什么信号都不会反向订阅 —— **结构上写不出来**。
语义对齐 Solid:只在声明的依赖变化时触发;回调收 (value, previous);
支持 { defer: true };依赖本身仍被追踪(不是 untrack 的替代品)。
## VOBS_C106 —— async 回调交给 effect/生命周期
只检测**直接**形态(effect(async () => …)、onMount/renderEffect/memo/on 同),
纯语法可见、零成本零误报 —— 与 Svelte 5 对 $effect 里 await 的处理同思路。
**间接形态有意不检测**:effect(() => { void someAsyncFn() }) 里被调函数的
async-ness 在另一个模块,纯 AST 看不出来。要检测必须引入 ts.Program +
TypeChecker,而编译器的 transform 是**逐文件**的(Vite 需求),那会破坏架构并
大幅拖慢构建。间接形态交给运行时护栏 C210/C211 —— 定位准确,是同行少有的能力。
## 组件可返回空值(业界一致做法)
运行时从 1.8.0 起就把空返回值归一化成空注释节点,但 VobsComponent 仍声明
=> VobsNode,于是 return cond ? <div/> : null **运行时正常、类型却报错**。
同类框架的组件返回类型**全都**含空值:ReactNode / Preact 的 ComponentChild /
Solid 的 JSX.Element / Vue 的 VNodeChild。现按同一做法放宽。
VobsNode 本身**不放宽**:它仍表示"真节点",给 insertBefore 传 null 依旧是错的。
连带修 ComponentProps:它的条件类型写死了 => VobsNode,放宽后不再匹配任何组件、
静默回落成 Record<string, never>(表现为 createComponent 的 props 变空对象类型)。
## 修 HMR 刷新路径的空返回值崩溃
createComponent 有两条渲染路径,1.8.0 只修了首屏那条,refreshInstance 没修:
nodeOwners.set(next as object, owner) 拿到 null 当 WeakMap 的 key → 抛
Invalid value used as weak map key(与 9/30 发版黑屏同一个错误)。
hmr.ts 把 refresh 的异常包在 try/catch 里,所以后果不是崩溃而是
**热更新静默不生效、屏幕停在旧版本** —— 那个 catch 自己的注释写着这是最容易被
当成「改了没生效」查半天的形态。开发期问题。
## 验证
- on.test.ts(11 例):确定性复现"effect 订阅被调函数读的信号"这一环 ·
on() 之后写该信号不再触发 · 依赖变化照常触发 · previous 语义 · defer ·
依赖数组 · 取值函数 · 回调内读取不订阅 · 传普通值抛错 · cleanup 透出 ·
与 untrack 的区别
- async-effect.test.ts(11 例):5 种应报形态(含 severity=warning 与 fix 文案)·
5 种不该报形态
- component-empty-hmr.test.ts(3 例):类型层直接写不需 cast · 早退式同 ·
**走真实 HMR 路径**(用 resolveComponent 拿代理,实例才真的注册)
**咬合的教训**:HMR 那条测试我第一版是**没牙齿**的 —— 直接 createComponent +
updateHmrModule,实例从未注册到该 moduleId,module.instances 是空的、refresh
根本没跑,测试全绿但什么都没验(退掉归一化也不变红)。改用 resolveComponent
后才真的走到那条路径,并补了"内容真的变成空"的判据证明 refresh 执行过。
全量门禁 1369 测试全绿 · typecheck 0 · check:source 0 · verify:packages 0。