@vobs/runtime 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/async-boundary.cjs +73 -374
  2. package/dist/async-boundary.cjs.map +1 -1
  3. package/dist/async-boundary.js +44 -367
  4. package/dist/async-boundary.js.map +1 -1
  5. package/dist/bind.cjs +101 -234
  6. package/dist/bind.cjs.map +1 -1
  7. package/dist/bind.js +72 -227
  8. package/dist/bind.js.map +1 -1
  9. package/dist/boundary.cjs +67 -369
  10. package/dist/boundary.cjs.map +1 -1
  11. package/dist/boundary.js +42 -366
  12. package/dist/boundary.js.map +1 -1
  13. package/dist/debug.cjs +41 -12
  14. package/dist/debug.cjs.map +1 -1
  15. package/dist/debug.js +10 -3
  16. package/dist/debug.js.map +1 -1
  17. package/dist/dynamic.cjs +76 -378
  18. package/dist/dynamic.cjs.map +1 -1
  19. package/dist/dynamic.js +43 -367
  20. package/dist/dynamic.js.map +1 -1
  21. package/dist/error-boundary.cjs +77 -372
  22. package/dist/error-boundary.cjs.map +1 -1
  23. package/dist/error-boundary.d.cts +7 -3
  24. package/dist/error-boundary.d.ts +7 -3
  25. package/dist/error-boundary.js +48 -367
  26. package/dist/error-boundary.js.map +1 -1
  27. package/dist/error.cjs +35 -9
  28. package/dist/error.cjs.map +1 -1
  29. package/dist/error.js +7 -3
  30. package/dist/error.js.map +1 -1
  31. package/dist/fragment.cjs +44 -133
  32. package/dist/fragment.cjs.map +1 -1
  33. package/dist/fragment.js +19 -130
  34. package/dist/fragment.js.map +1 -1
  35. package/dist/hmr.cjs +61 -22
  36. package/dist/hmr.cjs.map +1 -1
  37. package/dist/hmr.d.cts +7 -1
  38. package/dist/hmr.d.ts +7 -1
  39. package/dist/hmr.js +19 -3
  40. package/dist/hmr.js.map +1 -1
  41. package/dist/index.cjs +230 -465
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +1 -1
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +128 -388
  46. package/dist/index.js.map +1 -1
  47. package/dist/ops.cjs +147 -244
  48. package/dist/ops.cjs.map +1 -1
  49. package/dist/ops.js +97 -216
  50. package/dist/ops.js.map +1 -1
  51. package/dist/profiler.cjs +69 -320
  52. package/dist/profiler.cjs.map +1 -1
  53. package/dist/profiler.js +40 -315
  54. package/dist/profiler.js.map +1 -1
  55. package/dist/ref.cjs +33 -131
  56. package/dist/ref.cjs.map +1 -1
  57. package/dist/ref.js +10 -130
  58. package/dist/ref.js.map +1 -1
  59. package/dist/renderer.cjs +18 -2
  60. package/dist/renderer.cjs.map +1 -1
  61. package/dist/renderer.js +0 -2
  62. package/dist/renderer.js.map +1 -1
  63. package/dist/template.cjs +29 -6
  64. package/dist/template.cjs.map +1 -1
  65. package/dist/template.js +4 -3
  66. package/dist/template.js.map +1 -1
  67. package/package.json +2 -2
  68. package/src/dynamic.test.ts +65 -1
  69. package/src/error-boundary.ts +13 -3
  70. package/src/hmr.test.ts +17 -0
  71. package/src/hmr.ts +14 -0
  72. package/src/index.ts +1 -0
  73. package/src/ops.ts +38 -3
  74. package/src/props.test.ts +32 -1
package/dist/ref.js CHANGED
@@ -1,134 +1,8 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
- // packages/reactivity/src/debug.ts
5
- var activeDebugHooks = null;
6
- function hasDebugHooks() {
7
- return activeDebugHooks !== null;
8
- }
9
- __name(hasDebugHooks, "hasDebugHooks");
10
- function invokeDebug(name, ...args) {
11
- return;
12
- }
13
- __name(invokeDebug, "invokeDebug");
14
-
15
- // packages/reactivity/src/owner.ts
16
- var currentOwner = null;
17
- function getCurrentOwner() {
18
- return currentOwner;
19
- }
20
- __name(getCurrentOwner, "getCurrentOwner");
21
-
22
- // packages/reactivity/src/scheduler.ts
23
- var _Scheduler = class _Scheduler {
24
- constructor() {
25
- this.dirtyEffects = /* @__PURE__ */ new Set();
26
- this.lowPriorityEffects = /* @__PURE__ */ new Set();
27
- // flush 不可重入(flushing 标志保证),缓冲数组可在轮次间安全复用,避免每轮分配。
28
- this.normalBuffer = [];
29
- this.lowBuffer = [];
30
- this.flushing = false;
31
- this.scheduled = false;
32
- this.batchDepth = 0;
33
- }
34
- schedule(effect2) {
35
- if (effect2.disposed) return;
36
- this.dirtyEffects.add(effect2);
37
- this.lowPriorityEffects.delete(effect2);
38
- this.ensureScheduled();
39
- }
40
- /** Queue an effect behind normal updates while preserving deterministic order. */
41
- scheduleLow(effect2) {
42
- if (effect2.disposed) return;
43
- if (!this.dirtyEffects.has(effect2)) this.lowPriorityEffects.add(effect2);
44
- this.ensureScheduled();
45
- }
46
- ensureScheduled() {
47
- if (this.batchDepth === 0 && !this.flushing && !this.scheduled) {
48
- this.scheduled = true;
49
- queueMicrotask(() => {
50
- this.scheduled = false;
51
- this.flush();
52
- });
53
- }
54
- }
55
- remove(effect2) {
56
- this.dirtyEffects.delete(effect2);
57
- this.lowPriorityEffects.delete(effect2);
58
- }
59
- batch(fn) {
60
- this.batchDepth++;
61
- try {
62
- return fn();
63
- } finally {
64
- this.batchDepth--;
65
- if (this.batchDepth === 0) this.flush();
66
- }
67
- }
68
- flush() {
69
- if (this.flushing || this.batchDepth > 0) return;
70
- this.flushing = true;
71
- let rounds = 0;
72
- let firstError;
73
- let hasError = false;
74
- try {
75
- while (this.dirtyEffects.size > 0 || this.lowPriorityEffects.size > 0) {
76
- if (++rounds > 100) {
77
- this.dirtyEffects.clear();
78
- this.lowPriorityEffects.clear();
79
- throw new Error("Vobs: \u54CD\u5E94\u5F0F\u66F4\u65B0\u8D85\u8FC7 100 \u8F6E\uFF0C\u53EF\u80FD\u5B58\u5728\u5FAA\u73AF\u4F9D\u8D56");
80
- }
81
- this.collectRunnable(this.dirtyEffects, this.normalBuffer);
82
- this.collectRunnable(this.lowPriorityEffects, this.lowBuffer);
83
- sortEffects(this.normalBuffer);
84
- sortEffects(this.lowBuffer);
85
- for (const effect2 of this.normalBuffer) {
86
- try {
87
- effect2.run();
88
- } catch (error) {
89
- if (!hasError) {
90
- firstError = error;
91
- hasError = true;
92
- }
93
- }
94
- }
95
- for (const effect2 of this.lowBuffer) {
96
- try {
97
- effect2.run();
98
- } catch (error) {
99
- if (!hasError) {
100
- firstError = error;
101
- hasError = true;
102
- }
103
- }
104
- }
105
- this.normalBuffer.length = 0;
106
- this.lowBuffer.length = 0;
107
- }
108
- } finally {
109
- this.normalBuffer.length = 0;
110
- this.lowBuffer.length = 0;
111
- this.flushing = false;
112
- }
113
- if (hasError) throw firstError;
114
- }
115
- /** 收集未 disposed 的 effect 并清空源集合;run() 期间新调度的 effect 留给下一轮。 */
116
- collectRunnable(source, target) {
117
- for (const effect2 of source) {
118
- if (!effect2.disposed) target.push(effect2);
119
- }
120
- source.clear();
121
- }
122
- };
123
- __name(_Scheduler, "Scheduler");
124
- function sortEffects(effects) {
125
- if (effects.length > 1) {
126
- effects.sort((a, b) => b.depth - a.depth || a.order - b.order);
127
- }
128
- }
129
- __name(sortEffects, "sortEffects");
130
-
131
4
  // packages/runtime/src/ref.ts
5
+ import { getCurrentOwner } from "@vobs/reactivity";
132
6
  function ref(initialValue = null) {
133
7
  return { current: initialValue };
134
8
  }
@@ -136,7 +10,12 @@ __name(ref, "ref");
136
10
  function setRef(node, target) {
137
11
  if (!isRefTarget(target)) return;
138
12
  if (isSSRNode(node)) return;
13
+ const owner = getCurrentOwner();
139
14
  assignRef(target, node);
15
+ owner?.onDispose(() => {
16
+ if (isObjectRef(target) && target.current !== node) return;
17
+ assignRef(target, null);
18
+ });
140
19
  }
141
20
  __name(setRef, "setRef");
142
21
  function isSSRNode(value) {
@@ -160,7 +39,8 @@ function assignRef(target, value) {
160
39
  }
161
40
  }
162
41
  __name(assignRef, "assignRef");
163
-
164
- export { ref, setRef };
165
- //# sourceMappingURL=ref.js.map
42
+ export {
43
+ ref,
44
+ setRef
45
+ };
166
46
  //# sourceMappingURL=ref.js.map
package/dist/ref.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../reactivity/src/debug.ts","../../reactivity/src/owner.ts","../../reactivity/src/scheduler.ts","../src/ref.ts"],"names":["effect"],"mappings":"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","file":"ref.js","sourcesContent":["import type { Effect } from './effect'\nimport type { Owner } from './owner'\nimport type { Dependency, ReadableSignal, Subscriber } from './signal'\n\nexport interface ReactivityDebugHooks {\n ownerCreated?(owner: Owner): void\n ownerNamed?(owner: Owner, name: string): void\n ownerDisposed?(owner: Owner): void\n signalCreated?(signal: ReadableSignal<unknown>, owner: Owner | null): void\n signalNamed?(signal: ReadableSignal<unknown>, name: string): void\n signalRead?(signal: ReadableSignal<unknown>, subscriber: Subscriber): void\n signalChanged?(signal: ReadableSignal<unknown>, previousValue: unknown, nextValue: unknown): void\n signalDisposed?(signal: ReadableSignal<unknown>): void\n dependencyTracked?(dependency: Dependency, subscriber: Subscriber): void\n dependencyUntracked?(dependency: Dependency, subscriber: Subscriber): void\n effectCreated?(effect: Effect, owner: Owner | null): void\n effectInvalidated?(effect: Effect): void\n effectRunStart?(effect: Effect): void\n effectRunEnd?(effect: Effect, error?: unknown, handled?: boolean): void\n effectDisposed?(effect: Effect): void\n memoCreated?(signal: ReadableSignal<unknown>, subscriber: Subscriber, owner: Owner | null): void\n memoInvalidated?(signal: ReadableSignal<unknown>): void\n flushStart?(): void\n flushEnd?(): void\n}\n\nlet activeDebugHooks: ReactivityDebugHooks | null = null\nconst signalNames = new WeakMap<object, string>()\n\nexport function setDebugHooks(hooks: ReactivityDebugHooks | null): ReactivityDebugHooks | null {\n const previous = activeDebugHooks\n activeDebugHooks = hooks\n return previous\n}\n\nexport function getDebugHooks(): ReactivityDebugHooks | null {\n return activeDebugHooks\n}\n\n/** 热路径守卫:无 hooks 时跳过 invokeDebug 调用,避免 rest 参数数组分配。 */\nexport function hasDebugHooks(): boolean {\n return activeDebugHooks !== null\n}\n\nexport function setSignalDebugName(signal: ReadableSignal<unknown>, name: string): void {\n signalNames.set(signal, name)\n invokeDebug('signalNamed', signal, name)\n}\n\nexport function getSignalDebugName(signal: ReadableSignal<unknown>): string | undefined {\n return signalNames.get(signal)\n}\n\nexport function invokeDebug<K extends keyof ReactivityDebugHooks>(\n name: K,\n ...args: Parameters<NonNullable<ReactivityDebugHooks[K]>>\n): void {\n const callback = activeDebugHooks?.[name] as ((...values: unknown[]) => void) | undefined\n if (!callback) return\n try {\n callback(...args)\n } catch {\n // Debug tooling must never change application behavior.\n }\n}\n","import { invokeDebug, hasDebugHooks } from './debug'\n\nexport interface Owner {\n readonly id: string\n readonly parent: Owner | null\n readonly children: readonly Owner[]\n readonly depth: number\n readonly disposed: boolean\n run<T>(fn: () => T): T\n addCleanup(cleanup: () => void): void\n onDispose(cleanup: () => void): void\n onError(handler: (error: unknown) => void): () => void\n handleError(error: unknown): boolean\n dispose(): void\n}\n\nlet currentOwner: Owner | null = null\nlet nextOwnerId = 1\nconst ownerNames = new WeakMap<Owner, string>()\n\nexport function createOwner(): Owner {\n const parent = currentOwner\n let disposed = parent?.disposed ?? false\n const children: Owner[] = []\n const cleanups: Array<() => void> = []\n const errorHandlers = new Set<(error: unknown) => void>()\n\n const owner: Owner = {\n id: `owner-${nextOwnerId++}`,\n parent,\n children,\n depth: (parent?.depth ?? -1) + 1,\n\n get disposed(): boolean {\n return disposed\n },\n\n run<T>(fn: () => T): T {\n if (disposed) throw new Error('Vobs: 已销毁的 Owner 不能继续运行')\n const previous = currentOwner\n currentOwner = owner\n try {\n return fn()\n } finally {\n currentOwner = previous\n }\n },\n\n addCleanup(cleanup: () => void): void {\n if (disposed) {\n cleanup()\n return\n }\n cleanups.push(cleanup)\n },\n\n onDispose(cleanup: () => void): void {\n owner.addCleanup(cleanup)\n },\n\n onError(handler: (error: unknown) => void): () => void {\n errorHandlers.add(handler)\n const remove = () => errorHandlers.delete(handler)\n owner.addCleanup(remove)\n return remove\n },\n\n handleError(error: unknown): boolean {\n for (const handler of [...errorHandlers].reverse()) {\n try {\n handler(error)\n return true\n } catch (handlerError) {\n return parent?.handleError(handlerError) ?? false\n }\n }\n return parent?.handleError(error) ?? false\n },\n\n dispose(): void {\n if (disposed) return\n disposed = true\n for (const child of [...children]) child.dispose()\n children.length = 0\n\n let firstError: unknown\n for (let index = cleanups.length - 1; index >= 0; index--) {\n try {\n cleanups[index]()\n } catch (error) {\n firstError ??= error\n }\n }\n cleanups.length = 0\n\n if (parent) {\n const index = parent.children.indexOf(owner)\n if (index >= 0) (parent.children as Owner[]).splice(index, 1)\n }\n if (hasDebugHooks()) invokeDebug('ownerDisposed', owner)\n if (firstError) throw firstError\n }\n }\n\n if (parent && !parent.disposed) (parent.children as Owner[]).push(owner)\n if (hasDebugHooks()) invokeDebug('ownerCreated', owner)\n return owner\n}\n\nexport function setOwnerDebugName(owner: Owner, name: string): void {\n ownerNames.set(owner, name)\n if (hasDebugHooks()) invokeDebug('ownerNamed', owner, name)\n}\n\nexport function getOwnerDebugName(owner: Owner): string | undefined {\n return ownerNames.get(owner)\n}\n\nexport function getCurrentOwner(): Owner | null {\n return currentOwner\n}\n\n/** Re-enter an Owner for an explicitly captured asynchronous continuation. */\nexport function runWithOwner<T>(owner: Owner, fn: () => T): T {\n return owner.run(fn)\n}\n\nexport function onDispose(cleanup: () => void): void {\n const owner = getCurrentOwner()\n if (!owner) throw new Error('Vobs: onDispose 必须在 Owner 作用域内调用')\n owner.onDispose(cleanup)\n}\n","import type { Effect } from './effect'\nimport { hasDebugHooks, invokeDebug } from './debug'\n\nexport class Scheduler {\n private readonly dirtyEffects = new Set<Effect>()\n private readonly lowPriorityEffects = new Set<Effect>()\n // flush 不可重入(flushing 标志保证),缓冲数组可在轮次间安全复用,避免每轮分配。\n private readonly normalBuffer: Effect[] = []\n private readonly lowBuffer: Effect[] = []\n private flushing = false\n private scheduled = false\n private batchDepth = 0\n\n schedule(effect: Effect): void {\n if (effect.disposed) return\n this.dirtyEffects.add(effect)\n this.lowPriorityEffects.delete(effect)\n this.ensureScheduled()\n }\n\n /** Queue an effect behind normal updates while preserving deterministic order. */\n scheduleLow(effect: Effect): void {\n if (effect.disposed) return\n if (!this.dirtyEffects.has(effect)) this.lowPriorityEffects.add(effect)\n this.ensureScheduled()\n }\n\n private ensureScheduled(): void {\n if (this.batchDepth === 0 && !this.flushing && !this.scheduled) {\n this.scheduled = true\n queueMicrotask(() => {\n this.scheduled = false\n this.flush()\n })\n }\n }\n\n remove(effect: Effect): void {\n this.dirtyEffects.delete(effect)\n this.lowPriorityEffects.delete(effect)\n }\n\n batch<T>(fn: () => T): T {\n this.batchDepth++\n try {\n return fn()\n } finally {\n this.batchDepth--\n if (this.batchDepth === 0) this.flush()\n }\n }\n\n flush(): void {\n if (this.flushing || this.batchDepth > 0) return\n this.flushing = true\n const debugEnabled = hasDebugHooks()\n if (debugEnabled) invokeDebug('flushStart')\n let rounds = 0\n let firstError: unknown\n let hasError = false\n try {\n while (this.dirtyEffects.size > 0 || this.lowPriorityEffects.size > 0) {\n if (++rounds > 100) {\n this.dirtyEffects.clear()\n this.lowPriorityEffects.clear()\n throw new Error('Vobs: 响应式更新超过 100 轮,可能存在循环依赖')\n }\n\n // normal 先、low 后,两组分别排序:与旧的合并排序语义一致\n // (旧比较器里 low 优先级本就高于 depth),但消除了比较器内的 Set 查找。\n this.collectRunnable(this.dirtyEffects, this.normalBuffer)\n this.collectRunnable(this.lowPriorityEffects, this.lowBuffer)\n sortEffects(this.normalBuffer)\n sortEffects(this.lowBuffer)\n for (const effect of this.normalBuffer) {\n try {\n effect.run()\n } catch (error) {\n // Isolate one failed effect from the rest of the flush. Re-throw\n // the first error after all runnable effects have had a chance.\n if (!hasError) {\n firstError = error\n hasError = true\n }\n }\n }\n for (const effect of this.lowBuffer) {\n try {\n effect.run()\n } catch (error) {\n if (!hasError) {\n firstError = error\n hasError = true\n }\n }\n }\n this.normalBuffer.length = 0\n this.lowBuffer.length = 0\n }\n } finally {\n this.normalBuffer.length = 0\n this.lowBuffer.length = 0\n this.flushing = false\n if (debugEnabled) invokeDebug('flushEnd')\n }\n if (hasError) throw firstError\n }\n\n /** 收集未 disposed 的 effect 并清空源集合;run() 期间新调度的 effect 留给下一轮。 */\n private collectRunnable(source: Set<Effect>, target: Effect[]): void {\n for (const effect of source) {\n if (!effect.disposed) target.push(effect)\n }\n source.clear()\n }\n}\n\n/** depth 深的先跑(子 effect 先于父),同深度按创建顺序。 */\nfunction sortEffects(effects: Effect[]): void {\n if (effects.length > 1) {\n effects.sort((a, b) => b.depth - a.depth || a.order - b.order)\n }\n}\n\nexport const scheduler = new Scheduler()\n\nexport function batch<T>(fn: () => T): T {\n return scheduler.batch(fn)\n}\n\n/** Schedule a low-priority effect explicitly. */\nexport function scheduleLow(effect: Effect): void {\n effect.scheduleLow()\n}\n","import { getCurrentOwner } from '@vobs/reactivity'\n\n/** A mutable reference populated when a host node is mounted. */\nexport interface Ref<T extends object = Node> {\n current: T | null\n}\n\nexport type RefTarget<T extends object = Node> = Ref<T> | ((value: T | null) => void)\n\nexport function ref<T extends object = Node>(initialValue: T | null = null): Ref<T> {\n return { current: initialValue }\n}\n\n/** Bind a host node to an object or callback ref and clear it with its Owner. */\nexport function setRef<T extends object>(node: T, target: unknown): void {\n if (!isRefTarget<T>(target)) return\n // SSR's serializable nodes are never exposed as live refs. Custom renderers\n // may use arbitrary host objects, so only exclude the known SSR shape.\n if (isSSRNode(node)) return\n const owner = getCurrentOwner()\n assignRef(target, node)\n owner?.onDispose(() => {\n // Do not clear a ref that has since been reassigned to another node.\n if (isObjectRef(target) && target.current !== node) return\n assignRef(target, null)\n })\n}\n\nfunction isSSRNode(value: object): boolean {\n const type = (value as { type?: unknown }).type\n return (type === 'element' || type === 'text' || type === 'comment')\n && !('nodeType' in value)\n}\n\nfunction isObjectRef<T extends object>(value: unknown): value is Ref<T> {\n return Boolean(value && typeof value === 'object' && 'current' in value)\n}\n\nfunction isRefTarget<T extends object>(value: unknown): value is RefTarget<T> {\n return isObjectRef<T>(value) || typeof value === 'function'\n}\n\nfunction assignRef<T extends object>(target: RefTarget<T>, value: T | null): void {\n try {\n if (typeof target === 'function') target(value)\n else target.current = value\n } catch {\n // Ref callbacks are user code; never make mounting fail because of them.\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/ref.ts"],"sourcesContent":["import { getCurrentOwner } from '@vobs/reactivity'\n\n/** A mutable reference populated when a host node is mounted. */\nexport interface Ref<T extends object = Node> {\n current: T | null\n}\n\nexport type RefTarget<T extends object = Node> = Ref<T> | ((value: T | null) => void)\n\nexport function ref<T extends object = Node>(initialValue: T | null = null): Ref<T> {\n return { current: initialValue }\n}\n\n/** Bind a host node to an object or callback ref and clear it with its Owner. */\nexport function setRef<T extends object>(node: T, target: unknown): void {\n if (!isRefTarget<T>(target)) return\n // SSR's serializable nodes are never exposed as live refs. Custom renderers\n // may use arbitrary host objects, so only exclude the known SSR shape.\n if (isSSRNode(node)) return\n const owner = getCurrentOwner()\n assignRef(target, node)\n owner?.onDispose(() => {\n // Do not clear a ref that has since been reassigned to another node.\n if (isObjectRef(target) && target.current !== node) return\n assignRef(target, null)\n })\n}\n\nfunction isSSRNode(value: object): boolean {\n const type = (value as { type?: unknown }).type\n return (type === 'element' || type === 'text' || type === 'comment')\n && !('nodeType' in value)\n}\n\nfunction isObjectRef<T extends object>(value: unknown): value is Ref<T> {\n return Boolean(value && typeof value === 'object' && 'current' in value)\n}\n\nfunction isRefTarget<T extends object>(value: unknown): value is RefTarget<T> {\n return isObjectRef<T>(value) || typeof value === 'function'\n}\n\nfunction assignRef<T extends object>(target: RefTarget<T>, value: T | null): void {\n try {\n if (typeof target === 'function') target(value)\n else target.current = value\n } catch {\n // Ref callbacks are user code; never make mounting fail because of them.\n }\n}\n"],"mappings":";;;;AAAA,SAAS,uBAAuB;AASzB,SAAS,IAA6B,eAAyB,MAAc;AAClF,SAAO,EAAE,SAAS,aAAa;AACjC;AAFgB;AAKT,SAAS,OAAyB,MAAS,QAAuB;AACvE,MAAI,CAAC,YAAe,MAAM,EAAG;AAG7B,MAAI,UAAU,IAAI,EAAG;AACrB,QAAM,QAAQ,gBAAgB;AAC9B,YAAU,QAAQ,IAAI;AACtB,SAAO,UAAU,MAAM;AAErB,QAAI,YAAY,MAAM,KAAK,OAAO,YAAY,KAAM;AACpD,cAAU,QAAQ,IAAI;AAAA,EACxB,CAAC;AACH;AAZgB;AAchB,SAAS,UAAU,OAAwB;AACzC,QAAM,OAAQ,MAA6B;AAC3C,UAAQ,SAAS,aAAa,SAAS,UAAU,SAAS,cACrD,EAAE,cAAc;AACvB;AAJS;AAMT,SAAS,YAA8B,OAAiC;AACtE,SAAO,QAAQ,SAAS,OAAO,UAAU,YAAY,aAAa,KAAK;AACzE;AAFS;AAIT,SAAS,YAA8B,OAAuC;AAC5E,SAAO,YAAe,KAAK,KAAK,OAAO,UAAU;AACnD;AAFS;AAIT,SAAS,UAA4B,QAAsB,OAAuB;AAChF,MAAI;AACF,QAAI,OAAO,WAAW,WAAY,QAAO,KAAK;AAAA,QACzC,QAAO,UAAU;AAAA,EACxB,QAAQ;AAAA,EAER;AACF;AAPS;","names":[]}
package/dist/renderer.cjs CHANGED
@@ -1,4 +1,20 @@
1
- 'use strict';
1
+ var __VOBS_CJS_FILE_URL = require("url").pathToFileURL(__filename).href;
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __copyProps = (to, from, except, desc) => {
8
+ if (from && typeof from === "object" || typeof from === "function") {
9
+ for (let key of __getOwnPropNames(from))
10
+ if (!__hasOwnProp.call(to, key) && key !== except)
11
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
12
+ }
13
+ return to;
14
+ };
15
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
2
16
 
3
- //# sourceMappingURL=renderer.cjs.map
17
+ // packages/runtime/src/renderer.ts
18
+ var renderer_exports = {};
19
+ module.exports = __toCommonJS(renderer_exports);
4
20
  //# sourceMappingURL=renderer.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"renderer.cjs"}
1
+ {"version":3,"sources":["../src/renderer.ts"],"sourcesContent":["export interface VobsRenderer<\n NodeType = Node,\n TextNode extends NodeType = NodeType,\n ElementNode extends NodeType = NodeType,\n CommentNode extends NodeType = NodeType\n> {\n createText(content: string): TextNode\n createElement(tag: string): ElementNode\n createComment(content: string): CommentNode\n insertBefore(parent: NodeType, child: NodeType, anchor: NodeType | null): void\n removeChild(parent: NodeType, child: NodeType): void\n setTextContent(node: TextNode, content: string): void\n setProperty(node: ElementNode, key: string, value: unknown): void\n setAttribute(node: ElementNode, key: string, value: string): void\n addEventListener(node: ElementNode, event: string, handler: EventListener): void\n removeEventListener(node: ElementNode, event: string, handler: EventListener): void\n nextSibling(node: NodeType): NodeType | null\n clear(container: NodeType): void\n beginHydration?(): void\n completeHydration?(): void\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
package/dist/renderer.js CHANGED
@@ -1,3 +1 @@
1
-
2
- //# sourceMappingURL=renderer.js.map
3
1
  //# sourceMappingURL=renderer.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"renderer.js"}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/template.cjs CHANGED
@@ -1,9 +1,31 @@
1
- 'use strict';
2
-
1
+ var __VOBS_CJS_FILE_URL = require("url").pathToFileURL(__filename).href;
2
+ "use strict";
3
3
  var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
7
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
5
21
 
6
22
  // packages/runtime/src/template.ts
23
+ var template_exports = {};
24
+ __export(template_exports, {
25
+ cloneTemplate: () => cloneTemplate,
26
+ createTemplate: () => createTemplate
27
+ });
28
+ module.exports = __toCommonJS(template_exports);
7
29
  var templateCache = /* @__PURE__ */ new Map();
8
30
  function createTemplate(html) {
9
31
  let template = templateCache.get(html);
@@ -21,8 +43,9 @@ function cloneTemplate(template) {
21
43
  return root.cloneNode(true);
22
44
  }
23
45
  __name(cloneTemplate, "cloneTemplate");
24
-
25
- exports.cloneTemplate = cloneTemplate;
26
- exports.createTemplate = createTemplate;
27
- //# sourceMappingURL=template.cjs.map
46
+ // Annotate the CommonJS export names for ESM import in node:
47
+ 0 && (module.exports = {
48
+ cloneTemplate,
49
+ createTemplate
50
+ });
28
51
  //# sourceMappingURL=template.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/template.ts"],"names":[],"mappings":";;;;;;AAKA,IAAM,aAAA,uBAAoB,GAAA,EAAiC;AAGpD,SAAS,eAAe,IAAA,EAAmC;AAChE,EAAA,IAAI,QAAA,GAAW,aAAA,CAAc,GAAA,CAAI,IAAI,CAAA;AACrC,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,QAAA,GAAW,QAAA,CAAS,cAAc,UAAU,CAAA;AAC5C,IAAA,QAAA,CAAS,SAAA,GAAY,IAAA;AACrB,IAAA,aAAA,CAAc,GAAA,CAAI,MAAM,QAAQ,CAAA;AAAA,EAClC;AACA,EAAA,OAAO,QAAA;AACT;AARgB,MAAA,CAAA,cAAA,EAAA,gBAAA,CAAA;AAWT,SAAS,cAAc,QAAA,EAAqC;AACjE,EAAA,MAAM,IAAA,GAAO,SAAS,OAAA,CAAQ,iBAAA;AAC9B,EAAA,IAAI,CAAC,IAAA,EAAM,MAAM,IAAI,MAAM,8GAAyB,CAAA;AACpD,EAAA,OAAO,IAAA,CAAK,UAAU,IAAI,CAAA;AAC5B;AAJgB,MAAA,CAAA,aAAA,EAAA,eAAA,CAAA","file":"template.cjs","sourcesContent":["// 编译器静态模板提升的运行时支撑:\n// 编译期把完全静态的 JSX 子树序列化为 HTML 字符串,模块加载时解析一次,\n// 运行时通过 cloneNode 复制,替代逐个 createElement + setStaticProps + insertBefore。\n// 仅适用于 DOM 渲染器(模板本质是 HTML),自定义渲染器场景由编译产物不使用该优化兜底。\n\nconst templateCache = new Map<string, HTMLTemplateElement>()\n\n/** Parse a static template once; called at module load for each hoisted template. */\nexport function createTemplate(html: string): HTMLTemplateElement {\n let template = templateCache.get(html)\n if (!template) {\n template = document.createElement('template')\n template.innerHTML = html\n templateCache.set(html, template)\n }\n return template\n}\n\n/** Clone a hoisted template for one mount; the compiled HTML always has a single root element. */\nexport function cloneTemplate(template: HTMLTemplateElement): Node {\n const root = template.content.firstElementChild\n if (!root) throw new Error('Vobs: 静态模板缺少根元素,请检查编译产物')\n return root.cloneNode(true)\n}\n"]}
1
+ {"version":3,"sources":["../src/template.ts"],"sourcesContent":["// 编译器静态模板提升的运行时支撑:\n// 编译期把完全静态的 JSX 子树序列化为 HTML 字符串,模块加载时解析一次,\n// 运行时通过 cloneNode 复制,替代逐个 createElement + setStaticProps + insertBefore。\n// 仅适用于 DOM 渲染器(模板本质是 HTML),自定义渲染器场景由编译产物不使用该优化兜底。\n\nconst templateCache = new Map<string, HTMLTemplateElement>()\n\n/** Parse a static template once; called at module load for each hoisted template. */\nexport function createTemplate(html: string): HTMLTemplateElement {\n let template = templateCache.get(html)\n if (!template) {\n template = document.createElement('template')\n template.innerHTML = html\n templateCache.set(html, template)\n }\n return template\n}\n\n/** Clone a hoisted template for one mount; the compiled HTML always has a single root element. */\nexport function cloneTemplate(template: HTMLTemplateElement): Node {\n const root = template.content.firstElementChild\n if (!root) throw new Error('Vobs: 静态模板缺少根元素,请检查编译产物')\n return root.cloneNode(true)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,IAAM,gBAAgB,oBAAI,IAAiC;AAGpD,SAAS,eAAe,MAAmC;AAChE,MAAI,WAAW,cAAc,IAAI,IAAI;AACrC,MAAI,CAAC,UAAU;AACb,eAAW,SAAS,cAAc,UAAU;AAC5C,aAAS,YAAY;AACrB,kBAAc,IAAI,MAAM,QAAQ;AAAA,EAClC;AACA,SAAO;AACT;AARgB;AAWT,SAAS,cAAc,UAAqC;AACjE,QAAM,OAAO,SAAS,QAAQ;AAC9B,MAAI,CAAC,KAAM,OAAM,IAAI,MAAM,8GAAyB;AACpD,SAAO,KAAK,UAAU,IAAI;AAC5B;AAJgB;","names":[]}
package/dist/template.js CHANGED
@@ -19,7 +19,8 @@ function cloneTemplate(template) {
19
19
  return root.cloneNode(true);
20
20
  }
21
21
  __name(cloneTemplate, "cloneTemplate");
22
-
23
- export { cloneTemplate, createTemplate };
24
- //# sourceMappingURL=template.js.map
22
+ export {
23
+ cloneTemplate,
24
+ createTemplate
25
+ };
25
26
  //# sourceMappingURL=template.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/template.ts"],"names":[],"mappings":";;;;AAKA,IAAM,aAAA,uBAAoB,GAAA,EAAiC;AAGpD,SAAS,eAAe,IAAA,EAAmC;AAChE,EAAA,IAAI,QAAA,GAAW,aAAA,CAAc,GAAA,CAAI,IAAI,CAAA;AACrC,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,QAAA,GAAW,QAAA,CAAS,cAAc,UAAU,CAAA;AAC5C,IAAA,QAAA,CAAS,SAAA,GAAY,IAAA;AACrB,IAAA,aAAA,CAAc,GAAA,CAAI,MAAM,QAAQ,CAAA;AAAA,EAClC;AACA,EAAA,OAAO,QAAA;AACT;AARgB,MAAA,CAAA,cAAA,EAAA,gBAAA,CAAA;AAWT,SAAS,cAAc,QAAA,EAAqC;AACjE,EAAA,MAAM,IAAA,GAAO,SAAS,OAAA,CAAQ,iBAAA;AAC9B,EAAA,IAAI,CAAC,IAAA,EAAM,MAAM,IAAI,MAAM,8GAAyB,CAAA;AACpD,EAAA,OAAO,IAAA,CAAK,UAAU,IAAI,CAAA;AAC5B;AAJgB,MAAA,CAAA,aAAA,EAAA,eAAA,CAAA","file":"template.js","sourcesContent":["// 编译器静态模板提升的运行时支撑:\n// 编译期把完全静态的 JSX 子树序列化为 HTML 字符串,模块加载时解析一次,\n// 运行时通过 cloneNode 复制,替代逐个 createElement + setStaticProps + insertBefore。\n// 仅适用于 DOM 渲染器(模板本质是 HTML),自定义渲染器场景由编译产物不使用该优化兜底。\n\nconst templateCache = new Map<string, HTMLTemplateElement>()\n\n/** Parse a static template once; called at module load for each hoisted template. */\nexport function createTemplate(html: string): HTMLTemplateElement {\n let template = templateCache.get(html)\n if (!template) {\n template = document.createElement('template')\n template.innerHTML = html\n templateCache.set(html, template)\n }\n return template\n}\n\n/** Clone a hoisted template for one mount; the compiled HTML always has a single root element. */\nexport function cloneTemplate(template: HTMLTemplateElement): Node {\n const root = template.content.firstElementChild\n if (!root) throw new Error('Vobs: 静态模板缺少根元素,请检查编译产物')\n return root.cloneNode(true)\n}\n"]}
1
+ {"version":3,"sources":["../src/template.ts"],"sourcesContent":["// 编译器静态模板提升的运行时支撑:\n// 编译期把完全静态的 JSX 子树序列化为 HTML 字符串,模块加载时解析一次,\n// 运行时通过 cloneNode 复制,替代逐个 createElement + setStaticProps + insertBefore。\n// 仅适用于 DOM 渲染器(模板本质是 HTML),自定义渲染器场景由编译产物不使用该优化兜底。\n\nconst templateCache = new Map<string, HTMLTemplateElement>()\n\n/** Parse a static template once; called at module load for each hoisted template. */\nexport function createTemplate(html: string): HTMLTemplateElement {\n let template = templateCache.get(html)\n if (!template) {\n template = document.createElement('template')\n template.innerHTML = html\n templateCache.set(html, template)\n }\n return template\n}\n\n/** Clone a hoisted template for one mount; the compiled HTML always has a single root element. */\nexport function cloneTemplate(template: HTMLTemplateElement): Node {\n const root = template.content.firstElementChild\n if (!root) throw new Error('Vobs: 静态模板缺少根元素,请检查编译产物')\n return root.cloneNode(true)\n}\n"],"mappings":";;;;AAKA,IAAM,gBAAgB,oBAAI,IAAiC;AAGpD,SAAS,eAAe,MAAmC;AAChE,MAAI,WAAW,cAAc,IAAI,IAAI;AACrC,MAAI,CAAC,UAAU;AACb,eAAW,SAAS,cAAc,UAAU;AAC5C,aAAS,YAAY;AACrB,kBAAc,IAAI,MAAM,QAAQ;AAAA,EAClC;AACA,SAAO;AACT;AARgB;AAWT,SAAS,cAAc,UAAqC;AACjE,QAAM,OAAO,SAAS,QAAQ;AAC9B,MAAI,CAAC,KAAM,OAAM,IAAI,MAAM,8GAAyB;AACpD,SAAO,KAAK,UAAU,IAAI;AAC5B;AAJgB;","names":[]}
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "type": "git",
12
12
  "url": "git+https://github.com/vobsjs/vobs.git"
13
13
  },
14
- "version": "1.2.1",
14
+ "version": "1.3.0",
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
@@ -39,6 +39,6 @@
39
39
  "./source/*": "./src/*"
40
40
  },
41
41
  "dependencies": {
42
- "@vobs/reactivity": "1.2.1"
42
+ "@vobs/reactivity": "1.3.0"
43
43
  }
44
44
  }
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment jsdom
2
2
  import { describe, expect, it } from 'vitest'
3
- import { createDOMRenderer, setRenderer, createText, insertDynamicValue, insertList, createComponent } from '@vobs/vobs'
3
+ import { createDOMRenderer, setRenderer, createText, insertDynamicValue, insertList, insertDynamic, addEventListener, createComponent } from '@vobs/vobs'
4
4
  import { effect, state } from '@vobs/reactivity'
5
5
  import { bindText } from './bind'
6
6
  import { createFragment, type VobsNode } from './fragment'
@@ -397,3 +397,67 @@ describe('keyed list minimal-move reconciliation', () => {
397
397
  })
398
398
  })
399
399
 
400
+
401
+ describe('component render isolation (untrack)', () => {
402
+ const flush = () => new Promise(resolve => setTimeout(resolve, 0))
403
+
404
+ it('组件体内的信号读取不成为 insertDynamic 的结构依赖', async () => {
405
+ setRenderer(createDOMRenderer())
406
+ const visible = state(false)
407
+ const unrelated = state(0)
408
+ let builds = 0
409
+ function Panel(): Node {
410
+ builds += 1
411
+ // 组件体读取信号:修复前会被追踪为祖先 effect 依赖,触发整树重建
412
+ unrelated.value
413
+ return document.createElement('input')
414
+ }
415
+ const parent = document.createElement('div')
416
+ insertDynamic(parent, null, () => (visible.value ? createComponent(Panel, {}) : null))
417
+ expect(builds).toBe(0)
418
+
419
+ visible.value = true
420
+ await flush()
421
+ expect(builds).toBe(1)
422
+ expect(parent.querySelector('input')).not.toBeNull()
423
+
424
+ unrelated.value = 99
425
+ await flush()
426
+ // 无关信号变化不重建子树
427
+ expect(builds).toBe(1)
428
+
429
+ visible.value = false
430
+ await flush()
431
+ // 条件工厂自身的依赖仍然生效
432
+ expect(parent.querySelector('input')).toBeNull()
433
+ expect(builds).toBe(1)
434
+ })
435
+
436
+ it('动态组件的事件监听在信号变化后持续可用', async () => {
437
+ setRenderer(createDOMRenderer())
438
+ const visible = state(true)
439
+ const content = state('')
440
+ function Field(): Node {
441
+ const input = document.createElement('input')
442
+ addEventListener(input, 'input', event => {
443
+ content.set((event.target as HTMLInputElement).value)
444
+ })
445
+ return input
446
+ }
447
+ const parent = document.createElement('div')
448
+ insertDynamic(parent, null, () => (visible.value ? createComponent(Field, {}) : null))
449
+ await flush()
450
+
451
+ const input = parent.querySelector('input')!
452
+ input.value = 'hello'
453
+ input.dispatchEvent(new Event('input'))
454
+ expect(content.value).toBe('hello')
455
+
456
+ // 触发一次渲染期读取过的信号变化后,事件监听依旧有效
457
+ visible.value = true
458
+ await flush()
459
+ input.value = 'world'
460
+ input.dispatchEvent(new Event('input'))
461
+ expect(content.value).toBe('world')
462
+ })
463
+ })
@@ -7,16 +7,26 @@ export type ErrorBoundaryFallback = (
7
7
  retry: BoundaryRetry
8
8
  ) => ReturnType<NodeFactory>
9
9
 
10
+ /**
11
+ * 子节点双形态:JSX 属性经编译器编译为惰性 getter(工厂),手写对象字面量可直接传节点。
12
+ */
13
+ export type ErrorBoundaryChildren = NodeFactory | VobsNode
14
+
10
15
  export interface ErrorBoundaryOptions {
11
- children: NodeFactory
16
+ children: ErrorBoundaryChildren
12
17
  fallback: ErrorBoundaryFallback
13
18
  }
14
19
 
15
20
  export interface ErrorBoundaryProps {
16
- children: NodeFactory
21
+ children: ErrorBoundaryChildren
17
22
  fallback: ErrorBoundaryFallback
18
23
  }
19
24
 
25
+ /** Normalize child sources to the lazy factory shape insertBoundary expects. */
26
+ function resolveBoundaryChildren(children: ErrorBoundaryChildren): NodeFactory {
27
+ return typeof children === 'function' ? children : () => children
28
+ }
29
+
20
30
  /**
21
31
  * Inserts an error boundary without adding a wrapper node. It captures errors
22
32
  * from its child render branch and child-owned effects, then renders fallback.
@@ -26,7 +36,7 @@ export function insertErrorBoundary(
26
36
  anchor: Node | null,
27
37
  options: ErrorBoundaryOptions
28
38
  ): void {
29
- insertBoundary(parent, anchor, options)
39
+ insertBoundary(parent, anchor, { ...options, children: resolveBoundaryChildren(options.children) })
30
40
  }
31
41
 
32
42
  /** Component-shaped API backed by the same no-wrapper host instruction. */
package/src/hmr.test.ts CHANGED
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
3
3
  import {
4
4
  createHmrStateStore,
5
5
  disposeHmrModule,
6
+ hmrStateRef,
6
7
  resolveComponent,
7
8
  updateHmrModule
8
9
  } from './hmr'
@@ -54,3 +55,19 @@ describe('runtime HMR', () => {
54
55
  expect(createHmrStateStore(moduleId).get('count', 0)).toBe(3)
55
56
  })
56
57
  })
58
+
59
+ describe('hmrStateRef', () => {
60
+ it('模块重执行时复用既有信号实例(状态保鲜)', () => {
61
+ let first: unknown
62
+ const create = () => ({ value: 0, tag: Math.random() })
63
+ // 模拟模块首执行
64
+ const firstRef = hmrStateRef('src/stores/a.ts', 'count', create)
65
+ first = firstRef
66
+ // 模拟热更新后模块重执行:新实例、同一 key
67
+ const secondRef = hmrStateRef('src/stores/a.ts', 'count', create)
68
+ expect(secondRef).toBe(first)
69
+ // 不同 key / 不同模块各自独立
70
+ expect(hmrStateRef('src/stores/a.ts', 'other', create)).not.toBe(first)
71
+ expect(hmrStateRef('src/stores/b.ts', 'count', create)).not.toBe(first)
72
+ })
73
+ })
package/src/hmr.ts CHANGED
@@ -96,6 +96,20 @@ export function createHmrStateStore(moduleId: string): HmrStateStore {
96
96
  }
97
97
  }
98
98
 
99
+ /**
100
+ * 编译器为模块顶层 state() 声明生成的取值入口(`#state:` 前缀与手动 store 键隔离)。
101
+ * 模块热更新重执行时复用既有信号实例:旧导入方持有的实例与新模块实例共享同一份状态,
102
+ * 消除"两份模块、两份状态"导致的编辑不生效/页面半边失灵。
103
+ */
104
+ export function hmrStateRef<T>(moduleId: string, key: string, create: () => T): T {
105
+ const state = getModule(moduleId).state
106
+ const registryKey = `#state:${key}`
107
+ if (state.has(registryKey)) return state.get(registryKey) as T
108
+ const value = create()
109
+ state.set(registryKey, value)
110
+ return value
111
+ }
112
+
99
113
  export function registerHmrInstance(moduleId: string, instance: HmrInstance): () => void {
100
114
  const instances = getModule(moduleId).instances
101
115
  instances.add(instance)
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export type {
65
65
  export {
66
66
  createHmrStateStore,
67
67
  disposeHmrModule,
68
+ hmrStateRef,
68
69
  markHmrInstanceMounted,
69
70
  registerHmrInstance,
70
71
  resolveComponent,
package/src/ops.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // 编译产物调用的基础操作
2
2
 
3
- import { createOwner, getCurrentOwner, setOwnerDebugName, type Owner } from '@vobs/reactivity'
3
+ import { createOwner, getCurrentOwner, setOwnerDebugName, untrack, type Owner } from '@vobs/reactivity'
4
4
  import { isVobsFragment, type VobsNode } from './fragment'
5
5
  import { describeDebugNode, getRuntimeDebugHooks, invokeRuntimeDebug, readDebugValue } from './debug'
6
6
  import {
@@ -118,6 +118,9 @@ export function setProperty(
118
118
  ? readDebugValue(() => Reflect.get(node, key))
119
119
  : undefined
120
120
  getRenderer().setProperty(node, key, value)
121
+ if (key === 'value' && (node as { tagName?: unknown }).tagName === 'SELECT') {
122
+ scheduleSelectValueSync(node, value)
123
+ }
121
124
  if (getRuntimeDebugHooks()) {
122
125
  invokeRuntimeDebug('domMutation', {
123
126
  operation: 'property',
@@ -129,6 +132,30 @@ export function setProperty(
129
132
  }
130
133
  }
131
134
 
135
+ const pendingSelectValues = new WeakMap<Element, unknown>()
136
+ const selectSyncScheduled = new WeakSet<Element>()
137
+
138
+ /**
139
+ * `<select value>` 在 option 子节点存在前赋值不生效(HTML 规范:select 的 value
140
+ * 由已存在的 option 决定)。编译产物先设置属性、后插入子节点,静态写法必然丢初始值
141
+ * (消费方此前只能用 ref + queueMicrotask 规避)。
142
+ * 这里对 select 的 value 赋值统一延迟到微任务重放一次:静态子节点在同一同步任务内
143
+ * 插入完毕,重放即命中。动态(insertList/insertDynamic)插入的 option 晚于该微任务时,
144
+ * 由 value 的绑定 effect 在后续信号更新中正常覆盖。
145
+ */
146
+ function scheduleSelectValueSync(node: Element, value: unknown): void {
147
+ pendingSelectValues.set(node, value)
148
+ if (selectSyncScheduled.has(node)) return
149
+ selectSyncScheduled.add(node)
150
+ queueMicrotask(() => {
151
+ selectSyncScheduled.delete(node)
152
+ if (!pendingSelectValues.has(node)) return
153
+ const pending = pendingSelectValues.get(node)
154
+ pendingSelectValues.delete(node)
155
+ getRenderer().setProperty(node, 'value', pending)
156
+ })
157
+ }
158
+
132
159
  export function setAttribute(
133
160
  node: Element,
134
161
  key: string,
@@ -207,6 +234,9 @@ export function addEventListener(
207
234
  if (previous && previous.original === handler && previous.owner === owner) return
208
235
  if (previous) renderer.removeEventListener(node, event, previous.handler)
209
236
  const listener = owner ? (reason: Event) => {
237
+ // Owner 已销毁说明节点所属子树已被卸载/替换,事件来自游离 DOM,直接忽略。
238
+ // 否则 owner.run 会抛"已销毁的 Owner",在事件流里制造无意义的错误噪音。
239
+ if (owner.disposed) return
210
240
  try {
211
241
  owner.run(() => handler(reason))
212
242
  } catch (error) {
@@ -273,7 +303,12 @@ export function createComponent<Component extends VobsComponent>(
273
303
  })
274
304
  let node: VobsNode
275
305
  try {
276
- node = owner.run(() => component(props))
306
+ // 组件渲染必须 untrack:组件是 run-once 的,其渲染发生在某次 effect 求值
307
+ // (insertDynamic/insertBoundary 的渲染工厂、路由挂载)内时,若不切断追踪,
308
+ // 组件体内读取的信号会被收集为祖先 effect 的依赖——一次无关编辑就会触发
309
+ // 整棵子树销毁重建(输入框被换掉、焦点丢失、事件监听随旧树一起被清理)。
310
+ // 结构性响应只属于条件工厂与绑定 effect,组件本体渲染一次即止。
311
+ node = owner.run(() => untrack(() => component(props)))
277
312
  } catch (error) {
278
313
  owner.dispose()
279
314
  attachSourceLocation(error, source)
@@ -288,7 +323,7 @@ export function createComponent<Component extends VobsComponent>(
288
323
  parent: null,
289
324
  refresh(): void {
290
325
  const previous = instance.node
291
- const next = owner.run(() => component(props))
326
+ const next = owner.run(() => untrack(() => component(props)))
292
327
  if (instance.parent && !isVobsFragment(previous) && !isVobsFragment(next)) {
293
328
  getRenderer().insertBefore(instance.parent, next, previous)
294
329
  getRenderer().removeChild(instance.parent, previous)
package/src/props.test.ts CHANGED
@@ -1,10 +1,41 @@
1
1
  // @vitest-environment jsdom
2
2
  import { describe, expect, it } from 'vitest'
3
3
  import { createDOMRenderer, setRenderer } from '@vobs/vobs'
4
- import { setStaticProps, spreadProps } from './index'
4
+ import { setProperty, setStaticProps, spreadProps } from './index'
5
5
 
6
6
  setRenderer(createDOMRenderer())
7
7
 
8
+ describe('select value sync', () => {
9
+ it('option 子节点就绪后重放 value 赋值(微任务)', async () => {
10
+ const select = document.createElement('select') as HTMLSelectElement
11
+ // 模拟编译产物顺序:先设置 value,后插入 option 子节点
12
+ setProperty(select, 'value', 'b')
13
+ select.innerHTML = '<option value="a">A</option><option value="b">B</option>'
14
+ // 子节点插入使 value 回落到首个 option
15
+ expect(select.value).toBe('a')
16
+ await Promise.resolve()
17
+ expect(select.value).toBe('b')
18
+ })
19
+
20
+ it('setStaticProps 的 value 走同一重放路径', async () => {
21
+ const select = document.createElement('select') as HTMLSelectElement
22
+ setStaticProps(select, { value: 'x' })
23
+ select.innerHTML = '<option value="x">X</option>'
24
+ expect(select.value).toBe('x')
25
+ await Promise.resolve()
26
+ expect(select.value).toBe('x')
27
+ })
28
+
29
+ it('同一微任务内多次赋值以最后一次为准', async () => {
30
+ const select = document.createElement('select') as HTMLSelectElement
31
+ setProperty(select, 'value', 'a')
32
+ setProperty(select, 'value', 'c')
33
+ select.innerHTML = '<option value="a">A</option><option value="c">C</option>'
34
+ await Promise.resolve()
35
+ expect(select.value).toBe('c')
36
+ })
37
+ })
38
+
8
39
  describe('static and spread prop application', () => {
9
40
  it('spreadProps applies false for property keys to clear them', () => {
10
41
  const node = document.createElement('input') as HTMLInputElement