Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit 9cc0a2b

Browse files
committed
feat: directive lifecycle hooks in v-for/v-if
1 parent 5a0365d commit 9cc0a2b

File tree

7 files changed

+641
-206
lines changed

7 files changed

+641
-206
lines changed

packages/runtime-vapor/__tests__/for.spec.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
import { createFor, nextTick, ref, renderEffect } from '../src'
1+
import { NOOP } from '@vue/shared'
2+
import {
3+
type Directive,
4+
children,
5+
createFor,
6+
nextTick,
7+
ref,
8+
renderEffect,
9+
template,
10+
unmountComponent,
11+
withDirectives,
12+
} from '../src'
213
import { makeRender } from './_utils'
314

415
const define = makeRender()
@@ -184,4 +195,86 @@ describe('createFor', () => {
184195
await nextTick()
185196
expect(host.innerHTML).toBe('<!--for-->')
186197
})
198+
199+
test('should work with directive hooks', async () => {
200+
const calls: string[] = []
201+
const list = ref([0])
202+
const update = ref(0)
203+
const add = () => list.value.push(list.value.length)
204+
205+
const vDirective: Directive = {
206+
created: (el, { value }) => calls.push(`${value} created`),
207+
beforeMount: (el, { value }) => calls.push(`${value} beforeMount`),
208+
mounted: (el, { value }) => calls.push(`${value} mounted`),
209+
beforeUpdate: (el, { value }) => calls.push(`${value} beforeUpdate`),
210+
updated: (el, { value }) => calls.push(`${value} updated`),
211+
beforeUnmount: (el, { value }) => calls.push(`${value} beforeUnmount`),
212+
unmounted: (el, { value }) => calls.push(`${value} unmounted`),
213+
}
214+
215+
const t0 = template('<p></p>')
216+
const { instance } = define(() => {
217+
const n1 = createFor(
218+
() => list.value,
219+
block => {
220+
const n2 = t0()
221+
const n3 = children(n2, 0)
222+
withDirectives(n3, [[vDirective, () => block.s[0]]])
223+
return [n2, NOOP]
224+
},
225+
)
226+
renderEffect(() => update.value)
227+
return [n1]
228+
}).render()
229+
230+
await nextTick()
231+
// `${item index} ${hook name}`
232+
expect(calls).toEqual(['0 created', '0 beforeMount', '0 mounted'])
233+
calls.length = 0
234+
235+
add()
236+
await nextTick()
237+
expect(calls).toEqual([
238+
'0 beforeUpdate',
239+
'1 created',
240+
'1 beforeMount',
241+
'0 updated',
242+
'1 mounted',
243+
])
244+
calls.length = 0
245+
246+
list.value.reverse()
247+
await nextTick()
248+
expect(calls).lengthOf(4)
249+
expect(calls[0]).includes('beforeUpdate')
250+
expect(calls[1]).includes('beforeUpdate')
251+
expect(calls[2]).includes('updated')
252+
expect(calls[3]).includes('updated')
253+
list.value.reverse()
254+
await nextTick()
255+
calls.length = 0
256+
257+
update.value++
258+
await nextTick()
259+
expect(calls).toEqual([
260+
'0 beforeUpdate',
261+
'1 beforeUpdate',
262+
'0 updated',
263+
'1 updated',
264+
])
265+
calls.length = 0
266+
267+
list.value.pop()
268+
await nextTick()
269+
expect(calls).toEqual([
270+
'0 beforeUpdate',
271+
'1 beforeUnmount',
272+
'0 updated',
273+
'1 unmounted',
274+
])
275+
calls.length = 0
276+
277+
unmountComponent(instance)
278+
expect(calls).toEqual(['0 beforeUnmount', '0 unmounted'])
279+
})
187280
})

packages/runtime-vapor/__tests__/if.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {
2+
children,
23
createIf,
34
insert,
45
nextTick,
56
ref,
67
renderEffect,
78
setText,
89
template,
10+
unmountComponent,
11+
withDirectives,
912
} from '../src'
1013
import type { Mock } from 'vitest'
1114
import { makeRender } from './_utils'
@@ -124,4 +127,97 @@ describe('createIf', () => {
124127
await nextTick()
125128
expect(host.innerHTML).toBe('<!--if-->')
126129
})
130+
131+
test('should work with directive hooks', async () => {
132+
const calls: string[] = []
133+
const show1 = ref(true)
134+
const show2 = ref(true)
135+
const update = ref(0)
136+
const vDirective: any = {
137+
created: (el: any, { value }: any) => calls.push(`${value} created`),
138+
beforeMount: (el: any, { value }: any) =>
139+
calls.push(`${value} beforeMount`),
140+
mounted: (el: any, { value }: any) => calls.push(`${value} mounted`),
141+
beforeUpdate: (el: any, { value }: any) =>
142+
calls.push(`${value} beforeUpdate`),
143+
updated: (el: any, { value }: any) => calls.push(`${value} updated`),
144+
beforeUnmount: (el: any, { value }: any) =>
145+
calls.push(`${value} beforeUnmount`),
146+
unmounted: (el: any, { value }: any) => calls.push(`${value} unmounted`),
147+
}
148+
149+
const t0 = template('<p></p>')
150+
const { instance } = define(() => {
151+
const n1 = createIf(
152+
() => show1.value,
153+
() => {
154+
const n2 = t0()
155+
withDirectives(children(n2, 0), [
156+
[vDirective, () => (update.value, '1')],
157+
])
158+
return n2
159+
},
160+
() =>
161+
createIf(
162+
() => show2.value,
163+
() => {
164+
const n2 = t0()
165+
withDirectives(children(n2, 0), [[vDirective, () => '2']])
166+
return n2
167+
},
168+
() => {
169+
const n2 = t0()
170+
withDirectives(children(n2, 0), [[vDirective, () => '3']])
171+
return n2
172+
},
173+
),
174+
)
175+
return [n1]
176+
}).render()
177+
178+
await nextTick()
179+
expect(calls).toEqual(['1 created', '1 beforeMount', '1 mounted'])
180+
calls.length = 0
181+
182+
show1.value = false
183+
await nextTick()
184+
expect(calls).toEqual([
185+
'1 beforeUnmount',
186+
'2 created',
187+
'2 beforeMount',
188+
'1 unmounted',
189+
'2 mounted',
190+
])
191+
calls.length = 0
192+
193+
show2.value = false
194+
await nextTick()
195+
expect(calls).toEqual([
196+
'2 beforeUnmount',
197+
'3 created',
198+
'3 beforeMount',
199+
'2 unmounted',
200+
'3 mounted',
201+
])
202+
calls.length = 0
203+
204+
show1.value = true
205+
await nextTick()
206+
expect(calls).toEqual([
207+
'3 beforeUnmount',
208+
'1 created',
209+
'1 beforeMount',
210+
'3 unmounted',
211+
'1 mounted',
212+
])
213+
calls.length = 0
214+
215+
update.value++
216+
await nextTick()
217+
expect(calls).toEqual(['1 beforeUpdate', '1 updated'])
218+
calls.length = 0
219+
220+
unmountComponent(instance)
221+
expect(calls).toEqual(['1 beforeUnmount', '1 unmounted'])
222+
})
127223
})

packages/runtime-vapor/src/directives.ts

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
1-
import { NOOP, isFunction } from '@vue/shared'
2-
import { type ComponentInternalInstance, currentInstance } from './component'
3-
import { pauseTracking, resetTracking } from '@vue/reactivity'
1+
import { isFunction } from '@vue/shared'
2+
import { type ComponentInternalInstance, getCurrentInstance } from './component'
3+
import {
4+
type EffectScope,
5+
getCurrentScope,
6+
pauseTracking,
7+
resetTracking,
8+
traverse,
9+
} from '@vue/reactivity'
410
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
5-
import { renderWatch } from './renderWatch'
11+
import { renderEffect } from './renderWatch'
612

713
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
814

9-
export interface DirectiveBinding<V = any, M extends string = string> {
15+
export interface DirectiveBinding<T = any, V = any, M extends string = string> {
1016
instance: ComponentInternalInstance
1117
source?: () => V
1218
value: V
1319
oldValue: V | null
1420
arg?: string
1521
modifiers?: DirectiveModifiers<M>
16-
dir: ObjectDirective<any, V>
22+
dir: ObjectDirective<T, V, M>
1723
}
1824

25+
export type DirectiveBindingsMap = Map<Node, DirectiveBinding[]>
26+
1927
export type DirectiveHook<
2028
T = any | null,
2129
V = any,
2230
M extends string = string,
23-
> = (node: T, binding: DirectiveBinding<V, M>) => void
31+
> = (node: T, binding: DirectiveBinding<T, V, M>) => void
2432

2533
// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
2634
// effect update -> `beforeUpdate` -> node updated -> `updated`
@@ -37,7 +45,7 @@ export type ObjectDirective<T = any, V = any, M extends string = string> = {
3745
[K in DirectiveHookName]?: DirectiveHook<T, V, M> | undefined
3846
} & {
3947
/** Watch value deeply */
40-
deep?: boolean
48+
deep?: boolean | number
4149
}
4250

4351
export type FunctionDirective<
@@ -62,18 +70,39 @@ export type DirectiveArguments = Array<
6270
]
6371
>
6472

73+
const bindingsWithScope = new WeakMap<EffectScope, DirectiveBindingsMap>()
74+
75+
export function getDirectivesMap(
76+
scope = getCurrentScope(),
77+
): DirectiveBindingsMap | undefined {
78+
const instance = getCurrentInstance()
79+
if (instance && instance.scope === scope) {
80+
return instance.dirs
81+
} else {
82+
return scope && bindingsWithScope.get(scope)
83+
}
84+
}
85+
86+
export function setDirectivesWithScopeMap(
87+
scope: EffectScope,
88+
bindings: DirectiveBindingsMap,
89+
) {
90+
bindingsWithScope.set(scope, bindings)
91+
}
92+
6593
export function withDirectives<T extends Node>(
6694
node: T,
6795
directives: DirectiveArguments,
6896
): T {
69-
if (!currentInstance) {
97+
const instance = getCurrentInstance()
98+
const parentBindings = getDirectivesMap()
99+
if (!instance || !parentBindings) {
70100
// TODO warning
71101
return node
72102
}
73103

74-
const instance = currentInstance
75-
if (!instance.dirs.has(node)) instance.dirs.set(node, [])
76-
const bindings = instance.dirs.get(node)!
104+
if (!parentBindings.has(node)) parentBindings.set(node, [])
105+
let bindings = parentBindings.get(node)!
77106

78107
for (const directive of directives) {
79108
let [dir, source, arg, modifiers] = directive
@@ -100,8 +129,13 @@ export function withDirectives<T extends Node>(
100129

101130
// register source
102131
if (source) {
132+
if (dir.deep) {
133+
const deep = dir.deep === true ? undefined : dir.deep
134+
const baseSource = source
135+
source = () => traverse(baseSource(), deep)
136+
}
103137
// callback will be overridden by middleware
104-
renderWatch(source, NOOP, { deep: dir.deep })
138+
renderEffect(source)
105139
}
106140
}
107141

@@ -111,13 +145,12 @@ export function withDirectives<T extends Node>(
111145
export function invokeDirectiveHook(
112146
instance: ComponentInternalInstance | null,
113147
name: DirectiveHookName,
114-
nodes?: IterableIterator<Node>,
148+
directives: DirectiveBindingsMap,
115149
) {
116150
if (!instance) return
117-
nodes = nodes || instance.dirs.keys()
118-
for (const node of nodes) {
119-
const directives = instance.dirs.get(node) || []
120-
for (const binding of directives) {
151+
const iterator = directives.entries()
152+
for (const [node, bindings] of iterator) {
153+
for (const binding of bindings) {
121154
callDirectiveHook(node, binding, instance, name)
122155
}
123156
}

0 commit comments

Comments
 (0)