Skip to content

Commit cfadb98

Browse files
committed
fix(runtime-core): rework vnode hooks handling
- peroperly support directive on components (e.g. <foo v-show="x">) - consistently invoke raw vnode hooks on component vnodes (fix #684)
1 parent 8a87074 commit cfadb98

File tree

4 files changed

+204
-26
lines changed

4 files changed

+204
-26
lines changed

packages/runtime-core/__tests__/directives.spec.ts

+156-14
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ describe('directives', () => {
9898
expect(prevVNode).toBe(null)
9999
}) as DirectiveHook)
100100

101+
const dir = {
102+
beforeMount,
103+
mounted,
104+
beforeUpdate,
105+
updated,
106+
beforeUnmount,
107+
unmounted
108+
}
109+
101110
let _instance: ComponentInternalInstance | null = null
102111
let _vnode: VNode | null = null
103112
let _prevVnode: VNode | null = null
@@ -109,14 +118,7 @@ describe('directives', () => {
109118
_prevVnode = _vnode
110119
_vnode = withDirectives(h('div', count.value), [
111120
[
112-
{
113-
beforeMount,
114-
mounted,
115-
beforeUpdate,
116-
updated,
117-
beforeUnmount,
118-
unmounted
119-
},
121+
dir,
120122
// value
121123
count.value,
122124
// argument
@@ -132,17 +134,17 @@ describe('directives', () => {
132134
const root = nodeOps.createElement('div')
133135
render(h(Comp), root)
134136

135-
expect(beforeMount).toHaveBeenCalled()
136-
expect(mounted).toHaveBeenCalled()
137+
expect(beforeMount).toHaveBeenCalledTimes(1)
138+
expect(mounted).toHaveBeenCalledTimes(1)
137139

138140
count.value++
139141
await nextTick()
140-
expect(beforeUpdate).toHaveBeenCalled()
141-
expect(updated).toHaveBeenCalled()
142+
expect(beforeUpdate).toHaveBeenCalledTimes(1)
143+
expect(updated).toHaveBeenCalledTimes(1)
142144

143145
render(null, root)
144-
expect(beforeUnmount).toHaveBeenCalled()
145-
expect(unmounted).toHaveBeenCalled()
146+
expect(beforeUnmount).toHaveBeenCalledTimes(1)
147+
expect(unmounted).toHaveBeenCalledTimes(1)
146148
})
147149

148150
it('should work with a function directive', async () => {
@@ -198,4 +200,144 @@ describe('directives', () => {
198200
await nextTick()
199201
expect(fn).toHaveBeenCalledTimes(2)
200202
})
203+
204+
it('should work on component vnode', async () => {
205+
const count = ref(0)
206+
207+
function assertBindings(binding: DirectiveBinding) {
208+
expect(binding.value).toBe(count.value)
209+
expect(binding.arg).toBe('foo')
210+
expect(binding.instance).toBe(_instance && _instance.proxy)
211+
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
212+
}
213+
214+
const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
215+
expect(el.tag).toBe('div')
216+
// should not be inserted yet
217+
expect(el.parentNode).toBe(null)
218+
expect(root.children.length).toBe(0)
219+
220+
assertBindings(binding)
221+
222+
expect(vnode.type).toBe(_vnode!.type)
223+
expect(prevVNode).toBe(null)
224+
}) as DirectiveHook)
225+
226+
const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
227+
expect(el.tag).toBe('div')
228+
// should be inserted now
229+
expect(el.parentNode).toBe(root)
230+
expect(root.children[0]).toBe(el)
231+
232+
assertBindings(binding)
233+
234+
expect(vnode.type).toBe(_vnode!.type)
235+
expect(prevVNode).toBe(null)
236+
}) as DirectiveHook)
237+
238+
const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
239+
expect(el.tag).toBe('div')
240+
expect(el.parentNode).toBe(root)
241+
expect(root.children[0]).toBe(el)
242+
243+
// node should not have been updated yet
244+
// expect(el.children[0].text).toBe(`${count.value - 1}`)
245+
246+
assertBindings(binding)
247+
248+
expect(vnode.type).toBe(_vnode!.type)
249+
expect(prevVNode!.type).toBe(_prevVnode!.type)
250+
}) as DirectiveHook)
251+
252+
const updated = jest.fn(((el, binding, vnode, prevVNode) => {
253+
expect(el.tag).toBe('div')
254+
expect(el.parentNode).toBe(root)
255+
expect(root.children[0]).toBe(el)
256+
257+
// node should have been updated
258+
expect(el.children[0].text).toBe(`${count.value}`)
259+
260+
assertBindings(binding)
261+
262+
expect(vnode.type).toBe(_vnode!.type)
263+
expect(prevVNode!.type).toBe(_prevVnode!.type)
264+
}) as DirectiveHook)
265+
266+
const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
267+
expect(el.tag).toBe('div')
268+
// should be removed now
269+
expect(el.parentNode).toBe(root)
270+
expect(root.children[0]).toBe(el)
271+
272+
assertBindings(binding)
273+
274+
expect(vnode.type).toBe(_vnode!.type)
275+
expect(prevVNode).toBe(null)
276+
}) as DirectiveHook)
277+
278+
const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
279+
expect(el.tag).toBe('div')
280+
// should have been removed
281+
expect(el.parentNode).toBe(null)
282+
expect(root.children.length).toBe(0)
283+
284+
assertBindings(binding)
285+
286+
expect(vnode.type).toBe(_vnode!.type)
287+
expect(prevVNode).toBe(null)
288+
}) as DirectiveHook)
289+
290+
const dir = {
291+
beforeMount,
292+
mounted,
293+
beforeUpdate,
294+
updated,
295+
beforeUnmount,
296+
unmounted
297+
}
298+
299+
let _instance: ComponentInternalInstance | null = null
300+
let _vnode: VNode | null = null
301+
let _prevVnode: VNode | null = null
302+
303+
const Child = (props: { count: number }) => {
304+
_prevVnode = _vnode
305+
_vnode = h('div', props.count)
306+
return _vnode
307+
}
308+
309+
const Comp = {
310+
setup() {
311+
_instance = currentInstance
312+
},
313+
render() {
314+
return withDirectives(h(Child, { count: count.value }), [
315+
[
316+
dir,
317+
// value
318+
count.value,
319+
// argument
320+
'foo',
321+
// modifiers
322+
{ ok: true }
323+
]
324+
])
325+
}
326+
}
327+
328+
const root = nodeOps.createElement('div')
329+
render(h(Comp), root)
330+
331+
expect(beforeMount).toHaveBeenCalledTimes(1)
332+
expect(mounted).toHaveBeenCalledTimes(1)
333+
334+
count.value++
335+
await nextTick()
336+
expect(beforeUpdate).toHaveBeenCalledTimes(1)
337+
expect(updated).toHaveBeenCalledTimes(1)
338+
339+
render(null, root)
340+
expect(beforeUnmount).toHaveBeenCalledTimes(1)
341+
expect(unmounted).toHaveBeenCalledTimes(1)
342+
})
201343
})

packages/runtime-core/src/component.ts

+2
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export interface ComponentInternalInstance {
113113
data: Data
114114
props: Data
115115
attrs: Data
116+
vnodeHooks: Data
116117
slots: Slots
117118
proxy: ComponentPublicInstance | null
118119
// alternative proxy used only for runtime-compiled render functions using
@@ -186,6 +187,7 @@ export function createComponentInstance(
186187
data: EMPTY_OBJ,
187188
props: EMPTY_OBJ,
188189
attrs: EMPTY_OBJ,
190+
vnodeHooks: EMPTY_OBJ,
189191
slots: EMPTY_OBJ,
190192
refs: EMPTY_OBJ,
191193

packages/runtime-core/src/componentProps.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
hasOwn,
1212
toRawType,
1313
PatchFlags,
14-
makeMap
14+
makeMap,
15+
isReservedProp
1516
} from '@vue/shared'
1617
import { warn } from './warning'
1718
import { Data, ComponentInternalInstance } from './component'
@@ -104,7 +105,8 @@ export function resolveProps(
104105

105106
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
106107
const props: Data = {}
107-
let attrs: Data | undefined = void 0
108+
let attrs: Data | undefined = undefined
109+
let vnodeHooks: Data | undefined = undefined
108110

109111
// update the instance propsProxy (passed to setup()) to trigger potential
110112
// changes
@@ -123,21 +125,28 @@ export function resolveProps(
123125

124126
if (rawProps != null) {
125127
for (const key in rawProps) {
128+
const value = rawProps[key]
126129
// key, ref are reserved and never passed down
127-
if (key === 'key' || key === 'ref') continue
130+
if (isReservedProp(key)) {
131+
if (key !== 'key' && key !== 'ref') {
132+
// vnode hooks.
133+
;(vnodeHooks || (vnodeHooks = {}))[key] = value
134+
}
135+
continue
136+
}
128137
// prop option names are camelized during normalization, so to support
129138
// kebab -> camel conversion here we need to camelize the key.
130139
if (hasDeclaredProps) {
131140
const camelKey = camelize(key)
132141
if (hasOwn(options, camelKey)) {
133-
setProp(camelKey, rawProps[key])
142+
setProp(camelKey, value)
134143
} else {
135144
// Any non-declared props are put into a separate `attrs` object
136145
// for spreading. Make sure to preserve original key casing
137-
;(attrs || (attrs = {}))[key] = rawProps[key]
146+
;(attrs || (attrs = {}))[key] = value
138147
}
139148
} else {
140-
setProp(key, rawProps[key])
149+
setProp(key, value)
141150
}
142151
}
143152
}
@@ -206,6 +215,7 @@ export function resolveProps(
206215

207216
instance.props = props
208217
instance.attrs = options ? attrs || EMPTY_OBJ : props
218+
instance.vnodeHooks = vnodeHooks || EMPTY_OBJ
209219
}
210220

211221
const normalizationMap = new WeakMap<

packages/runtime-core/src/componentRenderUtils.ts

+30-6
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function renderComponentRoot(
4646
props,
4747
slots,
4848
attrs,
49+
vnodeHooks,
4950
emit
5051
} = instance
5152

@@ -92,14 +93,23 @@ export function renderComponentRoot(
9293
}
9394
}
9495

96+
// inherit vnode hooks
97+
if (vnodeHooks !== EMPTY_OBJ) {
98+
result = cloneVNode(result, vnodeHooks)
99+
}
100+
// inherit directives
101+
if (vnode.dirs != null) {
102+
if (__DEV__ && !isElementRoot(result)) {
103+
warn(
104+
`Runtime directive used on component with non-element root node. ` +
105+
`The directives will not function as intended.`
106+
)
107+
}
108+
result.dirs = vnode.dirs
109+
}
95110
// inherit transition data
96111
if (vnode.transition != null) {
97-
if (
98-
__DEV__ &&
99-
!(result.shapeFlag & ShapeFlags.COMPONENT) &&
100-
!(result.shapeFlag & ShapeFlags.ELEMENT) &&
101-
result.type !== Comment
102-
) {
112+
if (__DEV__ && !isElementRoot(result)) {
103113
warn(
104114
`Component inside <Transition> renders non-element root node ` +
105115
`that cannot be animated.`
@@ -115,6 +125,14 @@ export function renderComponentRoot(
115125
return result
116126
}
117127

128+
function isElementRoot(vnode: VNode) {
129+
return (
130+
vnode.shapeFlag & ShapeFlags.COMPONENT ||
131+
vnode.shapeFlag & ShapeFlags.ELEMENT ||
132+
vnode.type === Comment // potential v-if branch switch
133+
)
134+
}
135+
118136
export function shouldUpdateComponent(
119137
prevVNode: VNode,
120138
nextVNode: VNode,
@@ -137,6 +155,11 @@ export function shouldUpdateComponent(
137155
return true
138156
}
139157

158+
// force child update on runtime directive usage on component vnode.
159+
if (nextVNode.dirs != null) {
160+
return true
161+
}
162+
140163
if (patchFlag > 0) {
141164
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
142165
// slot content that references values that might have changed,
@@ -174,6 +197,7 @@ export function shouldUpdateComponent(
174197
}
175198
return hasPropsChanged(prevProps, nextProps)
176199
}
200+
177201
return false
178202
}
179203

0 commit comments

Comments
 (0)