Skip to content

Commit cfca161

Browse files
LittleSoundsxzz
authored andcommitted
feat: directive lifecycle hooks in v-for/v-if
1 parent 6001fe8 commit cfca161

File tree

7 files changed

+505
-85
lines changed

7 files changed

+505
-85
lines changed

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
import { NOOP } from '@vue/shared'
12
import {
3+
type Directive,
4+
children,
25
computed,
36
createFor,
47
createTextNode,
58
nextTick,
69
ref,
710
renderEffect,
811
setText,
12+
template,
13+
unmountComponent,
14+
withDirectives,
915
} from '../src'
1016
import { makeRender } from './_utils'
1117

@@ -59,4 +65,86 @@ describe('createFor', () => {
5965
await nextTick()
6066
expect(host.innerHTML).toBe('<!--for-->')
6167
})
68+
69+
test('should work with directive hooks', async () => {
70+
const calls: string[] = []
71+
const list = ref([0])
72+
const update = ref(0)
73+
const add = () => list.value.push(list.value.length)
74+
75+
const vDirective: Directive = {
76+
created: (el, { value }) => calls.push(`${value} created`),
77+
beforeMount: (el, { value }) => calls.push(`${value} beforeMount`),
78+
mounted: (el, { value }) => calls.push(`${value} mounted`),
79+
beforeUpdate: (el, { value }) => calls.push(`${value} beforeUpdate`),
80+
updated: (el, { value }) => calls.push(`${value} updated`),
81+
beforeUnmount: (el, { value }) => calls.push(`${value} beforeUnmount`),
82+
unmounted: (el, { value }) => calls.push(`${value} unmounted`),
83+
}
84+
85+
const t0 = template('<p></p>')
86+
const { instance } = define(() => {
87+
const n1 = createFor(
88+
() => list.value,
89+
block => {
90+
const n2 = t0()
91+
const n3 = children(n2, 0)
92+
withDirectives(n3, [[vDirective, () => block.s[0]]])
93+
return [n2, NOOP]
94+
},
95+
)
96+
renderEffect(() => update.value)
97+
return [n1]
98+
}).render()
99+
100+
await nextTick()
101+
// `${item index} ${hook name}`
102+
expect(calls).toEqual(['0 created', '0 beforeMount', '0 mounted'])
103+
calls.length = 0
104+
105+
add()
106+
await nextTick()
107+
expect(calls).toEqual([
108+
'0 beforeUpdate',
109+
'1 created',
110+
'1 beforeMount',
111+
'0 updated',
112+
'1 mounted',
113+
])
114+
calls.length = 0
115+
116+
list.value.reverse()
117+
await nextTick()
118+
expect(calls).lengthOf(4)
119+
expect(calls[0]).includes('beforeUpdate')
120+
expect(calls[1]).includes('beforeUpdate')
121+
expect(calls[2]).includes('updated')
122+
expect(calls[3]).includes('updated')
123+
list.value.reverse()
124+
await nextTick()
125+
calls.length = 0
126+
127+
update.value++
128+
await nextTick()
129+
expect(calls).toEqual([
130+
'0 beforeUpdate',
131+
'1 beforeUpdate',
132+
'0 updated',
133+
'1 updated',
134+
])
135+
calls.length = 0
136+
137+
list.value.pop()
138+
await nextTick()
139+
expect(calls).toEqual([
140+
'0 beforeUpdate',
141+
'1 beforeUnmount',
142+
'0 updated',
143+
'1 unmounted',
144+
])
145+
calls.length = 0
146+
147+
unmountComponent(instance)
148+
expect(calls).toEqual(['0 beforeUnmount', '0 unmounted'])
149+
})
62150
})

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
renderEffect,
99
setText,
1010
template,
11+
unmountComponent,
12+
withDirectives,
1113
} from '../src'
1214
import type { Mock } from 'vitest'
1315
import { makeRender } from './_utils'
@@ -133,4 +135,97 @@ describe('createIf', () => {
133135
await nextTick()
134136
expect(host.innerHTML).toBe('<!--if-->')
135137
})
138+
139+
test('should work with directive hooks', async () => {
140+
const calls: string[] = []
141+
const show1 = ref(true)
142+
const show2 = ref(true)
143+
const update = ref(0)
144+
const vDirective: any = {
145+
created: (el: any, { value }: any) => calls.push(`${value} created`),
146+
beforeMount: (el: any, { value }: any) =>
147+
calls.push(`${value} beforeMount`),
148+
mounted: (el: any, { value }: any) => calls.push(`${value} mounted`),
149+
beforeUpdate: (el: any, { value }: any) =>
150+
calls.push(`${value} beforeUpdate`),
151+
updated: (el: any, { value }: any) => calls.push(`${value} updated`),
152+
beforeUnmount: (el: any, { value }: any) =>
153+
calls.push(`${value} beforeUnmount`),
154+
unmounted: (el: any, { value }: any) => calls.push(`${value} unmounted`),
155+
}
156+
157+
const t0 = template('<p></p>')
158+
const { instance } = define(() => {
159+
const n1 = createIf(
160+
() => show1.value,
161+
() => {
162+
const n2 = t0()
163+
withDirectives(children(n2, 0), [
164+
[vDirective, () => (update.value, '1')],
165+
])
166+
return n2
167+
},
168+
() =>
169+
createIf(
170+
() => show2.value,
171+
() => {
172+
const n2 = t0()
173+
withDirectives(children(n2, 0), [[vDirective, () => '2']])
174+
return n2
175+
},
176+
() => {
177+
const n2 = t0()
178+
withDirectives(children(n2, 0), [[vDirective, () => '3']])
179+
return n2
180+
},
181+
),
182+
)
183+
return [n1]
184+
}).render()
185+
186+
await nextTick()
187+
expect(calls).toEqual(['1 created', '1 beforeMount', '1 mounted'])
188+
calls.length = 0
189+
190+
show1.value = false
191+
await nextTick()
192+
expect(calls).toEqual([
193+
'1 beforeUnmount',
194+
'2 created',
195+
'2 beforeMount',
196+
'1 unmounted',
197+
'2 mounted',
198+
])
199+
calls.length = 0
200+
201+
show2.value = false
202+
await nextTick()
203+
expect(calls).toEqual([
204+
'2 beforeUnmount',
205+
'3 created',
206+
'3 beforeMount',
207+
'2 unmounted',
208+
'3 mounted',
209+
])
210+
calls.length = 0
211+
212+
show1.value = true
213+
await nextTick()
214+
expect(calls).toEqual([
215+
'3 beforeUnmount',
216+
'1 created',
217+
'1 beforeMount',
218+
'3 unmounted',
219+
'1 mounted',
220+
])
221+
calls.length = 0
222+
223+
update.value++
224+
await nextTick()
225+
expect(calls).toEqual(['1 beforeUpdate', '1 updated'])
226+
calls.length = 0
227+
228+
unmountComponent(instance)
229+
expect(calls).toEqual(['1 beforeUnmount', '1 unmounted'])
230+
})
136231
})

packages/runtime-vapor/src/directive.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)