fix(runtime-core/refs): handle multiple merged refs for dynamic component with vnode

fix #2078
This commit is contained in:
Evan You 2020-09-14 15:33:38 -04:00
parent 313dd06065
commit 612eb6712a
3 changed files with 86 additions and 11 deletions

View File

@ -6,7 +6,8 @@ import {
nextTick, nextTick,
defineComponent, defineComponent,
reactive, reactive,
serializeInner serializeInner,
shallowRef
} from '@vue/runtime-test' } from '@vue/runtime-test'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#template-refs // reference: https://vue-composition-api-rfc.netlify.com/api.html#template-refs
@ -325,4 +326,43 @@ describe('api: template refs', () => {
await nextTick() await nextTick()
expect(spy.mock.calls[1][0]).toBe('p') expect(spy.mock.calls[1][0]).toBe('p')
}) })
// #2078
test('handling multiple merged refs', async () => {
const Foo = {
render: () => h('div', 'foo')
}
const Bar = {
render: () => h('div', 'bar')
}
const viewRef = shallowRef<any>(Foo)
const elRef1 = ref()
const elRef2 = ref()
const App = {
render() {
if (!viewRef.value) {
return null
}
const view = h(viewRef.value, { ref: elRef1 })
return h(view, { ref: elRef2 })
}
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(elRef1.value.$el)).toBe('foo')
expect(elRef1.value).toBe(elRef2.value)
viewRef.value = Bar
await nextTick()
expect(serializeInner(elRef1.value.$el)).toBe('bar')
expect(elRef1.value).toBe(elRef2.value)
viewRef.value = null
await nextTick()
expect(elRef1.value).toBeNull()
expect(elRef1.value).toBe(elRef2.value)
})
}) })

View File

@ -10,7 +10,8 @@ import {
isSameVNodeType, isSameVNodeType,
Static, Static,
VNodeNormalizedRef, VNodeNormalizedRef,
VNodeHook VNodeHook,
VNodeNormalizedRefAtom
} from './vnode' } from './vnode'
import { import {
ComponentInternalInstance, ComponentInternalInstance,
@ -284,6 +285,19 @@ export const setRef = (
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
vnode: VNode | null vnode: VNode | null
) => { ) => {
if (isArray(rawRef)) {
rawRef.forEach((r, i) =>
setRef(
r,
oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
parentComponent,
parentSuspense,
vnode
)
)
return
}
let value: ComponentPublicInstance | RendererNode | null let value: ComponentPublicInstance | RendererNode | null
if (!vnode) { if (!vnode) {
value = null value = null
@ -295,7 +309,7 @@ export const setRef = (
} }
} }
const [owner, ref] = rawRef const { i: owner, r: ref } = rawRef
if (__DEV__ && !owner) { if (__DEV__ && !owner) {
warn( warn(
`Missing ref owner context. ref cannot be used on hoisted vnodes. ` + `Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
@ -303,7 +317,7 @@ export const setRef = (
) )
return return
} }
const oldRef = oldRawRef && oldRawRef[1] const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
const setupState = owner.setupState const setupState = owner.setupState

View File

@ -64,7 +64,14 @@ export type VNodeRef =
| Ref | Ref
| ((ref: object | null, refs: Record<string, any>) => void) | ((ref: object | null, refs: Record<string, any>) => void)
export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef] export type VNodeNormalizedRefAtom = {
i: ComponentInternalInstance
r: VNodeRef
}
export type VNodeNormalizedRef =
| VNodeNormalizedRefAtom
| (VNodeNormalizedRefAtom)[]
type VNodeMountHook = (vnode: VNode) => void type VNodeMountHook = (vnode: VNode) => void
type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void
@ -289,11 +296,11 @@ export const InternalObjectKey = `__vInternal`
const normalizeKey = ({ key }: VNodeProps): VNode['key'] => const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
key != null ? key : null key != null ? key : null
const normalizeRef = ({ ref }: VNodeProps): VNode['ref'] => { const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
return (ref != null return (ref != null
? isArray(ref) ? isArray(ref)
? ref ? ref
: [currentRenderingInstance!, ref] : { i: currentRenderingInstance, r: ref }
: null) as any : null) as any
} }
@ -317,7 +324,10 @@ function _createVNode(
} }
if (isVNode(type)) { if (isVNode(type)) {
const cloned = cloneVNode(type, props) // createVNode receiving an existing vnode. This happens in cases like
// <component :is="vnode"/>
// #2078 make sure to merge refs during the clone instead of overwriting it
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) { if (children) {
normalizeChildren(cloned, children) normalizeChildren(cloned, children)
} }
@ -429,11 +439,12 @@ function _createVNode(
export function cloneVNode<T, U>( export function cloneVNode<T, U>(
vnode: VNode<T, U>, vnode: VNode<T, U>,
extraProps?: Data & VNodeProps | null extraProps?: Data & VNodeProps | null,
mergeRef = false
): VNode<T, U> { ): VNode<T, U> {
// This is intentionally NOT using spread or extend to avoid the runtime // This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost. // key enumeration cost.
const { props, patchFlag } = vnode const { props, ref, patchFlag } = vnode
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
return { return {
__v_isVNode: true, __v_isVNode: true,
@ -441,7 +452,17 @@ export function cloneVNode<T, U>(
type: vnode.type, type: vnode.type,
props: mergedProps, props: mergedProps,
key: mergedProps && normalizeKey(mergedProps), key: mergedProps && normalizeKey(mergedProps),
ref: extraProps && extraProps.ref ? normalizeRef(extraProps) : vnode.ref, ref:
extraProps && extraProps.ref
? // #2078 in the case of <component :is="vnode" ref="extra"/>
// if the vnode itself already has a ref, cloneVNode will need to merge
// the refs so the single vnode can be set on multiple refs
mergeRef && ref
? isArray(ref)
? ref.concat(normalizeRef(extraProps)!)
: [ref, normalizeRef(extraProps)!]
: normalizeRef(extraProps)
: ref,
scopeId: vnode.scopeId, scopeId: vnode.scopeId,
children: vnode.children, children: vnode.children,
target: vnode.target, target: vnode.target,