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)
This commit is contained in:
parent
8a87074df0
commit
cfadb98011
@ -98,6 +98,15 @@ describe('directives', () => {
|
|||||||
expect(prevVNode).toBe(null)
|
expect(prevVNode).toBe(null)
|
||||||
}) as DirectiveHook)
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const dir = {
|
||||||
|
beforeMount,
|
||||||
|
mounted,
|
||||||
|
beforeUpdate,
|
||||||
|
updated,
|
||||||
|
beforeUnmount,
|
||||||
|
unmounted
|
||||||
|
}
|
||||||
|
|
||||||
let _instance: ComponentInternalInstance | null = null
|
let _instance: ComponentInternalInstance | null = null
|
||||||
let _vnode: VNode | null = null
|
let _vnode: VNode | null = null
|
||||||
let _prevVnode: VNode | null = null
|
let _prevVnode: VNode | null = null
|
||||||
@ -109,14 +118,7 @@ describe('directives', () => {
|
|||||||
_prevVnode = _vnode
|
_prevVnode = _vnode
|
||||||
_vnode = withDirectives(h('div', count.value), [
|
_vnode = withDirectives(h('div', count.value), [
|
||||||
[
|
[
|
||||||
{
|
dir,
|
||||||
beforeMount,
|
|
||||||
mounted,
|
|
||||||
beforeUpdate,
|
|
||||||
updated,
|
|
||||||
beforeUnmount,
|
|
||||||
unmounted
|
|
||||||
},
|
|
||||||
// value
|
// value
|
||||||
count.value,
|
count.value,
|
||||||
// argument
|
// argument
|
||||||
@ -132,17 +134,17 @@ describe('directives', () => {
|
|||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
|
|
||||||
expect(beforeMount).toHaveBeenCalled()
|
expect(beforeMount).toHaveBeenCalledTimes(1)
|
||||||
expect(mounted).toHaveBeenCalled()
|
expect(mounted).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(beforeUpdate).toHaveBeenCalled()
|
expect(beforeUpdate).toHaveBeenCalledTimes(1)
|
||||||
expect(updated).toHaveBeenCalled()
|
expect(updated).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
render(null, root)
|
render(null, root)
|
||||||
expect(beforeUnmount).toHaveBeenCalled()
|
expect(beforeUnmount).toHaveBeenCalledTimes(1)
|
||||||
expect(unmounted).toHaveBeenCalled()
|
expect(unmounted).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should work with a function directive', async () => {
|
it('should work with a function directive', async () => {
|
||||||
@ -198,4 +200,144 @@ describe('directives', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
expect(fn).toHaveBeenCalledTimes(2)
|
expect(fn).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should work on component vnode', async () => {
|
||||||
|
const count = ref(0)
|
||||||
|
|
||||||
|
function assertBindings(binding: DirectiveBinding) {
|
||||||
|
expect(binding.value).toBe(count.value)
|
||||||
|
expect(binding.arg).toBe('foo')
|
||||||
|
expect(binding.instance).toBe(_instance && _instance.proxy)
|
||||||
|
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
// should not be inserted yet
|
||||||
|
expect(el.parentNode).toBe(null)
|
||||||
|
expect(root.children.length).toBe(0)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode.type).toBe(_vnode!.type)
|
||||||
|
expect(prevVNode).toBe(null)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
// should be inserted now
|
||||||
|
expect(el.parentNode).toBe(root)
|
||||||
|
expect(root.children[0]).toBe(el)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode.type).toBe(_vnode!.type)
|
||||||
|
expect(prevVNode).toBe(null)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
expect(el.parentNode).toBe(root)
|
||||||
|
expect(root.children[0]).toBe(el)
|
||||||
|
|
||||||
|
// node should not have been updated yet
|
||||||
|
// expect(el.children[0].text).toBe(`${count.value - 1}`)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode.type).toBe(_vnode!.type)
|
||||||
|
expect(prevVNode!.type).toBe(_prevVnode!.type)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const updated = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
expect(el.parentNode).toBe(root)
|
||||||
|
expect(root.children[0]).toBe(el)
|
||||||
|
|
||||||
|
// node should have been updated
|
||||||
|
expect(el.children[0].text).toBe(`${count.value}`)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode.type).toBe(_vnode!.type)
|
||||||
|
expect(prevVNode!.type).toBe(_prevVnode!.type)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
// should be removed now
|
||||||
|
expect(el.parentNode).toBe(root)
|
||||||
|
expect(root.children[0]).toBe(el)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode.type).toBe(_vnode!.type)
|
||||||
|
expect(prevVNode).toBe(null)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
// should have been removed
|
||||||
|
expect(el.parentNode).toBe(null)
|
||||||
|
expect(root.children.length).toBe(0)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode.type).toBe(_vnode!.type)
|
||||||
|
expect(prevVNode).toBe(null)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const dir = {
|
||||||
|
beforeMount,
|
||||||
|
mounted,
|
||||||
|
beforeUpdate,
|
||||||
|
updated,
|
||||||
|
beforeUnmount,
|
||||||
|
unmounted
|
||||||
|
}
|
||||||
|
|
||||||
|
let _instance: ComponentInternalInstance | null = null
|
||||||
|
let _vnode: VNode | null = null
|
||||||
|
let _prevVnode: VNode | null = null
|
||||||
|
|
||||||
|
const Child = (props: { count: number }) => {
|
||||||
|
_prevVnode = _vnode
|
||||||
|
_vnode = h('div', props.count)
|
||||||
|
return _vnode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
_instance = currentInstance
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return withDirectives(h(Child, { count: count.value }), [
|
||||||
|
[
|
||||||
|
dir,
|
||||||
|
// value
|
||||||
|
count.value,
|
||||||
|
// argument
|
||||||
|
'foo',
|
||||||
|
// modifiers
|
||||||
|
{ ok: true }
|
||||||
|
]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
|
||||||
|
expect(beforeMount).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mounted).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
count.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(beforeUpdate).toHaveBeenCalledTimes(1)
|
||||||
|
expect(updated).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
render(null, root)
|
||||||
|
expect(beforeUnmount).toHaveBeenCalledTimes(1)
|
||||||
|
expect(unmounted).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -113,6 +113,7 @@ export interface ComponentInternalInstance {
|
|||||||
data: Data
|
data: Data
|
||||||
props: Data
|
props: Data
|
||||||
attrs: Data
|
attrs: Data
|
||||||
|
vnodeHooks: Data
|
||||||
slots: Slots
|
slots: Slots
|
||||||
proxy: ComponentPublicInstance | null
|
proxy: ComponentPublicInstance | null
|
||||||
// alternative proxy used only for runtime-compiled render functions using
|
// alternative proxy used only for runtime-compiled render functions using
|
||||||
@ -186,6 +187,7 @@ export function createComponentInstance(
|
|||||||
data: EMPTY_OBJ,
|
data: EMPTY_OBJ,
|
||||||
props: EMPTY_OBJ,
|
props: EMPTY_OBJ,
|
||||||
attrs: EMPTY_OBJ,
|
attrs: EMPTY_OBJ,
|
||||||
|
vnodeHooks: EMPTY_OBJ,
|
||||||
slots: EMPTY_OBJ,
|
slots: EMPTY_OBJ,
|
||||||
refs: EMPTY_OBJ,
|
refs: EMPTY_OBJ,
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ import {
|
|||||||
hasOwn,
|
hasOwn,
|
||||||
toRawType,
|
toRawType,
|
||||||
PatchFlags,
|
PatchFlags,
|
||||||
makeMap
|
makeMap,
|
||||||
|
isReservedProp
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { Data, ComponentInternalInstance } from './component'
|
import { Data, ComponentInternalInstance } from './component'
|
||||||
@ -104,7 +105,8 @@ export function resolveProps(
|
|||||||
|
|
||||||
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
|
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
|
||||||
const props: Data = {}
|
const props: Data = {}
|
||||||
let attrs: Data | undefined = void 0
|
let attrs: Data | undefined = undefined
|
||||||
|
let vnodeHooks: Data | undefined = undefined
|
||||||
|
|
||||||
// update the instance propsProxy (passed to setup()) to trigger potential
|
// update the instance propsProxy (passed to setup()) to trigger potential
|
||||||
// changes
|
// changes
|
||||||
@ -123,21 +125,28 @@ export function resolveProps(
|
|||||||
|
|
||||||
if (rawProps != null) {
|
if (rawProps != null) {
|
||||||
for (const key in rawProps) {
|
for (const key in rawProps) {
|
||||||
|
const value = rawProps[key]
|
||||||
// key, ref are reserved and never passed down
|
// key, ref are reserved and never passed down
|
||||||
if (key === 'key' || key === 'ref') continue
|
if (isReservedProp(key)) {
|
||||||
|
if (key !== 'key' && key !== 'ref') {
|
||||||
|
// vnode hooks.
|
||||||
|
;(vnodeHooks || (vnodeHooks = {}))[key] = value
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
// prop option names are camelized during normalization, so to support
|
// prop option names are camelized during normalization, so to support
|
||||||
// kebab -> camel conversion here we need to camelize the key.
|
// kebab -> camel conversion here we need to camelize the key.
|
||||||
if (hasDeclaredProps) {
|
if (hasDeclaredProps) {
|
||||||
const camelKey = camelize(key)
|
const camelKey = camelize(key)
|
||||||
if (hasOwn(options, camelKey)) {
|
if (hasOwn(options, camelKey)) {
|
||||||
setProp(camelKey, rawProps[key])
|
setProp(camelKey, value)
|
||||||
} else {
|
} else {
|
||||||
// Any non-declared props are put into a separate `attrs` object
|
// Any non-declared props are put into a separate `attrs` object
|
||||||
// for spreading. Make sure to preserve original key casing
|
// for spreading. Make sure to preserve original key casing
|
||||||
;(attrs || (attrs = {}))[key] = rawProps[key]
|
;(attrs || (attrs = {}))[key] = value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setProp(key, rawProps[key])
|
setProp(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,6 +215,7 @@ export function resolveProps(
|
|||||||
|
|
||||||
instance.props = props
|
instance.props = props
|
||||||
instance.attrs = options ? attrs || EMPTY_OBJ : props
|
instance.attrs = options ? attrs || EMPTY_OBJ : props
|
||||||
|
instance.vnodeHooks = vnodeHooks || EMPTY_OBJ
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizationMap = new WeakMap<
|
const normalizationMap = new WeakMap<
|
||||||
|
@ -46,6 +46,7 @@ export function renderComponentRoot(
|
|||||||
props,
|
props,
|
||||||
slots,
|
slots,
|
||||||
attrs,
|
attrs,
|
||||||
|
vnodeHooks,
|
||||||
emit
|
emit
|
||||||
} = instance
|
} = instance
|
||||||
|
|
||||||
@ -92,14 +93,23 @@ export function renderComponentRoot(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inherit vnode hooks
|
||||||
|
if (vnodeHooks !== EMPTY_OBJ) {
|
||||||
|
result = cloneVNode(result, vnodeHooks)
|
||||||
|
}
|
||||||
|
// inherit directives
|
||||||
|
if (vnode.dirs != null) {
|
||||||
|
if (__DEV__ && !isElementRoot(result)) {
|
||||||
|
warn(
|
||||||
|
`Runtime directive used on component with non-element root node. ` +
|
||||||
|
`The directives will not function as intended.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result.dirs = vnode.dirs
|
||||||
|
}
|
||||||
// inherit transition data
|
// inherit transition data
|
||||||
if (vnode.transition != null) {
|
if (vnode.transition != null) {
|
||||||
if (
|
if (__DEV__ && !isElementRoot(result)) {
|
||||||
__DEV__ &&
|
|
||||||
!(result.shapeFlag & ShapeFlags.COMPONENT) &&
|
|
||||||
!(result.shapeFlag & ShapeFlags.ELEMENT) &&
|
|
||||||
result.type !== Comment
|
|
||||||
) {
|
|
||||||
warn(
|
warn(
|
||||||
`Component inside <Transition> renders non-element root node ` +
|
`Component inside <Transition> renders non-element root node ` +
|
||||||
`that cannot be animated.`
|
`that cannot be animated.`
|
||||||
@ -115,6 +125,14 @@ export function renderComponentRoot(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isElementRoot(vnode: VNode) {
|
||||||
|
return (
|
||||||
|
vnode.shapeFlag & ShapeFlags.COMPONENT ||
|
||||||
|
vnode.shapeFlag & ShapeFlags.ELEMENT ||
|
||||||
|
vnode.type === Comment // potential v-if branch switch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldUpdateComponent(
|
export function shouldUpdateComponent(
|
||||||
prevVNode: VNode,
|
prevVNode: VNode,
|
||||||
nextVNode: VNode,
|
nextVNode: VNode,
|
||||||
@ -137,6 +155,11 @@ export function shouldUpdateComponent(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// force child update on runtime directive usage on component vnode.
|
||||||
|
if (nextVNode.dirs != null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if (patchFlag > 0) {
|
if (patchFlag > 0) {
|
||||||
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
|
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
|
||||||
// slot content that references values that might have changed,
|
// slot content that references values that might have changed,
|
||||||
@ -174,6 +197,7 @@ export function shouldUpdateComponent(
|
|||||||
}
|
}
|
||||||
return hasPropsChanged(prevProps, nextProps)
|
return hasPropsChanged(prevProps, nextProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user