@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.
- package/dist/async-boundary.cjs +73 -374
- package/dist/async-boundary.cjs.map +1 -1
- package/dist/async-boundary.js +44 -367
- package/dist/async-boundary.js.map +1 -1
- package/dist/bind.cjs +101 -234
- package/dist/bind.cjs.map +1 -1
- package/dist/bind.js +72 -227
- package/dist/bind.js.map +1 -1
- package/dist/boundary.cjs +67 -369
- package/dist/boundary.cjs.map +1 -1
- package/dist/boundary.js +42 -366
- package/dist/boundary.js.map +1 -1
- package/dist/debug.cjs +41 -12
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.js +10 -3
- package/dist/debug.js.map +1 -1
- package/dist/dynamic.cjs +76 -378
- package/dist/dynamic.cjs.map +1 -1
- package/dist/dynamic.js +43 -367
- package/dist/dynamic.js.map +1 -1
- package/dist/error-boundary.cjs +77 -372
- package/dist/error-boundary.cjs.map +1 -1
- package/dist/error-boundary.d.cts +7 -3
- package/dist/error-boundary.d.ts +7 -3
- package/dist/error-boundary.js +48 -367
- package/dist/error-boundary.js.map +1 -1
- package/dist/error.cjs +35 -9
- package/dist/error.cjs.map +1 -1
- package/dist/error.js +7 -3
- package/dist/error.js.map +1 -1
- package/dist/fragment.cjs +44 -133
- package/dist/fragment.cjs.map +1 -1
- package/dist/fragment.js +19 -130
- package/dist/fragment.js.map +1 -1
- package/dist/hmr.cjs +61 -22
- package/dist/hmr.cjs.map +1 -1
- package/dist/hmr.d.cts +7 -1
- package/dist/hmr.d.ts +7 -1
- package/dist/hmr.js +19 -3
- package/dist/hmr.js.map +1 -1
- package/dist/index.cjs +230 -465
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +128 -388
- package/dist/index.js.map +1 -1
- package/dist/ops.cjs +147 -244
- package/dist/ops.cjs.map +1 -1
- package/dist/ops.js +97 -216
- package/dist/ops.js.map +1 -1
- package/dist/profiler.cjs +69 -320
- package/dist/profiler.cjs.map +1 -1
- package/dist/profiler.js +40 -315
- package/dist/profiler.js.map +1 -1
- package/dist/ref.cjs +33 -131
- package/dist/ref.cjs.map +1 -1
- package/dist/ref.js +10 -130
- package/dist/ref.js.map +1 -1
- package/dist/renderer.cjs +18 -2
- package/dist/renderer.cjs.map +1 -1
- package/dist/renderer.js +0 -2
- package/dist/renderer.js.map +1 -1
- package/dist/template.cjs +29 -6
- package/dist/template.cjs.map +1 -1
- package/dist/template.js +4 -3
- package/dist/template.js.map +1 -1
- package/package.json +2 -2
- package/src/dynamic.test.ts +65 -1
- package/src/error-boundary.ts +13 -3
- package/src/hmr.test.ts +17 -0
- package/src/hmr.ts +14 -0
- package/src/index.ts +1 -0
- package/src/ops.ts +38 -3
- 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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
// packages/runtime/src/renderer.ts
|
|
18
|
+
var renderer_exports = {};
|
|
19
|
+
module.exports = __toCommonJS(renderer_exports);
|
|
4
20
|
//# sourceMappingURL=renderer.cjs.map
|
package/dist/renderer.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"
|
|
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
package/dist/renderer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/template.cjs
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
package/dist/template.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/template.ts"],"
|
|
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
|
-
|
|
24
|
-
|
|
22
|
+
export {
|
|
23
|
+
cloneTemplate,
|
|
24
|
+
createTemplate
|
|
25
|
+
};
|
|
25
26
|
//# sourceMappingURL=template.js.map
|
package/dist/template.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/template.ts"],"
|
|
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.
|
|
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.
|
|
42
|
+
"@vobs/reactivity": "1.3.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/dynamic.test.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/error-boundary.ts
CHANGED
|
@@ -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:
|
|
16
|
+
children: ErrorBoundaryChildren
|
|
12
17
|
fallback: ErrorBoundaryFallback
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export interface ErrorBoundaryProps {
|
|
16
|
-
children:
|
|
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
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
|
-
|
|
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
|