@vobs/runtime 1.2.2 → 1.3.1
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.map +1 -1
- package/dist/async-boundary.js +1 -1
- package/dist/async-boundary.js.map +1 -1
- package/dist/bind.cjs +18 -0
- package/dist/bind.cjs.map +1 -1
- package/dist/bind.js +19 -1
- package/dist/bind.js.map +1 -1
- package/dist/boundary.cjs.map +1 -1
- package/dist/boundary.js +1 -1
- package/dist/boundary.js.map +1 -1
- package/dist/dynamic.cjs.map +1 -1
- package/dist/dynamic.js +1 -1
- package/dist/dynamic.js.map +1 -1
- package/dist/error-boundary.cjs +5 -1
- 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 +6 -2
- package/dist/error-boundary.js.map +1 -1
- package/dist/fragment.cjs.map +1 -1
- package/dist/fragment.js +1 -1
- package/dist/fragment.js.map +1 -1
- package/dist/hmr.cjs +11 -0
- 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 +10 -0
- package/dist/hmr.js.map +1 -1
- package/dist/index.cjs +37 -3
- 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 +37 -4
- package/dist/index.js.map +1 -1
- package/dist/ops.cjs +21 -2
- package/dist/ops.cjs.map +1 -1
- package/dist/ops.js +22 -3
- package/dist/ops.js.map +1 -1
- package/dist/profiler.cjs.map +1 -1
- package/dist/profiler.js +1 -1
- package/dist/profiler.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/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
|