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,
defineComponent,
reactive,
serializeInner
serializeInner,
shallowRef
} from '@vue/runtime-test'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#template-refs
@ -325,4 +326,43 @@ describe('api: template refs', () => {
await nextTick()
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,
Static,
VNodeNormalizedRef,
VNodeHook
VNodeHook,
VNodeNormalizedRefAtom
} from './vnode'
import {
ComponentInternalInstance,
@ -284,6 +285,19 @@ export const setRef = (
parentSuspense: SuspenseBoundary | 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
if (!vnode) {
value = null
@ -295,7 +309,7 @@ export const setRef = (
}
}
const [owner, ref] = rawRef
const { i: owner, r: ref } = rawRef
if (__DEV__ && !owner) {
warn(
`Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
@ -303,7 +317,7 @@ export const setRef = (
)
return
}
const oldRef = oldRawRef && oldRawRef[1]
const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
const setupState = owner.setupState

View File

@ -64,7 +64,14 @@ export type VNodeRef =
| Ref
| ((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 VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void
@ -289,11 +296,11 @@ export const InternalObjectKey = `__vInternal`
const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
key != null ? key : null
const normalizeRef = ({ ref }: VNodeProps): VNode['ref'] => {
const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
return (ref != null
? isArray(ref)
? ref
: [currentRenderingInstance!, ref]
: { i: currentRenderingInstance, r: ref }
: null) as any
}
@ -317,7 +324,10 @@ function _createVNode(
}
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) {
normalizeChildren(cloned, children)
}
@ -429,11 +439,12 @@ function _createVNode(
export function cloneVNode<T, U>(
vnode: VNode<T, U>,
extraProps?: Data & VNodeProps | null
extraProps?: Data & VNodeProps | null,
mergeRef = false
): VNode<T, U> {
// This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost.
const { props, patchFlag } = vnode
const { props, ref, patchFlag } = vnode
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
return {
__v_isVNode: true,
@ -441,7 +452,17 @@ export function cloneVNode<T, U>(
type: vnode.type,
props: 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,
children: vnode.children,
target: vnode.target,