Skip to content

Commit 612eb67

Browse files
committed
fix(runtime-core/refs): handle multiple merged refs for dynamic component with vnode
fix #2078
1 parent 313dd06 commit 612eb67

File tree

3 files changed

+86
-11
lines changed

3 files changed

+86
-11
lines changed

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

+41-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
nextTick,
77
defineComponent,
88
reactive,
9-
serializeInner
9+
serializeInner,
10+
shallowRef
1011
} from '@vue/runtime-test'
1112

1213
// reference: https://vue-composition-api-rfc.netlify.com/api.html#template-refs
@@ -325,4 +326,43 @@ describe('api: template refs', () => {
325326
await nextTick()
326327
expect(spy.mock.calls[1][0]).toBe('p')
327328
})
329+
330+
// #2078
331+
test('handling multiple merged refs', async () => {
332+
const Foo = {
333+
render: () => h('div', 'foo')
334+
}
335+
const Bar = {
336+
render: () => h('div', 'bar')
337+
}
338+
339+
const viewRef = shallowRef<any>(Foo)
340+
const elRef1 = ref()
341+
const elRef2 = ref()
342+
343+
const App = {
344+
render() {
345+
if (!viewRef.value) {
346+
return null
347+
}
348+
const view = h(viewRef.value, { ref: elRef1 })
349+
return h(view, { ref: elRef2 })
350+
}
351+
}
352+
const root = nodeOps.createElement('div')
353+
render(h(App), root)
354+
355+
expect(serializeInner(elRef1.value.$el)).toBe('foo')
356+
expect(elRef1.value).toBe(elRef2.value)
357+
358+
viewRef.value = Bar
359+
await nextTick()
360+
expect(serializeInner(elRef1.value.$el)).toBe('bar')
361+
expect(elRef1.value).toBe(elRef2.value)
362+
363+
viewRef.value = null
364+
await nextTick()
365+
expect(elRef1.value).toBeNull()
366+
expect(elRef1.value).toBe(elRef2.value)
367+
})
328368
})

packages/runtime-core/src/renderer.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
isSameVNodeType,
1111
Static,
1212
VNodeNormalizedRef,
13-
VNodeHook
13+
VNodeHook,
14+
VNodeNormalizedRefAtom
1415
} from './vnode'
1516
import {
1617
ComponentInternalInstance,
@@ -284,6 +285,19 @@ export const setRef = (
284285
parentSuspense: SuspenseBoundary | null,
285286
vnode: VNode | null
286287
) => {
288+
if (isArray(rawRef)) {
289+
rawRef.forEach((r, i) =>
290+
setRef(
291+
r,
292+
oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
293+
parentComponent,
294+
parentSuspense,
295+
vnode
296+
)
297+
)
298+
return
299+
}
300+
287301
let value: ComponentPublicInstance | RendererNode | null
288302
if (!vnode) {
289303
value = null
@@ -295,15 +309,15 @@ export const setRef = (
295309
}
296310
}
297311

298-
const [owner, ref] = rawRef
312+
const { i: owner, r: ref } = rawRef
299313
if (__DEV__ && !owner) {
300314
warn(
301315
`Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
302316
`A vnode with ref must be created inside the render function.`
303317
)
304318
return
305319
}
306-
const oldRef = oldRawRef && oldRawRef[1]
320+
const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
307321
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
308322
const setupState = owner.setupState
309323

packages/runtime-core/src/vnode.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,14 @@ export type VNodeRef =
6464
| Ref
6565
| ((ref: object | null, refs: Record<string, any>) => void)
6666

67-
export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef]
67+
export type VNodeNormalizedRefAtom = {
68+
i: ComponentInternalInstance
69+
r: VNodeRef
70+
}
71+
72+
export type VNodeNormalizedRef =
73+
| VNodeNormalizedRefAtom
74+
| (VNodeNormalizedRefAtom)[]
6875

6976
type VNodeMountHook = (vnode: VNode) => void
7077
type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void
@@ -289,11 +296,11 @@ export const InternalObjectKey = `__vInternal`
289296
const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
290297
key != null ? key : null
291298

292-
const normalizeRef = ({ ref }: VNodeProps): VNode['ref'] => {
299+
const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
293300
return (ref != null
294301
? isArray(ref)
295302
? ref
296-
: [currentRenderingInstance!, ref]
303+
: { i: currentRenderingInstance, r: ref }
297304
: null) as any
298305
}
299306

@@ -317,7 +324,10 @@ function _createVNode(
317324
}
318325

319326
if (isVNode(type)) {
320-
const cloned = cloneVNode(type, props)
327+
// createVNode receiving an existing vnode. This happens in cases like
328+
// <component :is="vnode"/>
329+
// #2078 make sure to merge refs during the clone instead of overwriting it
330+
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
321331
if (children) {
322332
normalizeChildren(cloned, children)
323333
}
@@ -429,19 +439,30 @@ function _createVNode(
429439

430440
export function cloneVNode<T, U>(
431441
vnode: VNode<T, U>,
432-
extraProps?: Data & VNodeProps | null
442+
extraProps?: Data & VNodeProps | null,
443+
mergeRef = false
433444
): VNode<T, U> {
434445
// This is intentionally NOT using spread or extend to avoid the runtime
435446
// key enumeration cost.
436-
const { props, patchFlag } = vnode
447+
const { props, ref, patchFlag } = vnode
437448
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
438449
return {
439450
__v_isVNode: true,
440451
[ReactiveFlags.SKIP]: true,
441452
type: vnode.type,
442453
props: mergedProps,
443454
key: mergedProps && normalizeKey(mergedProps),
444-
ref: extraProps && extraProps.ref ? normalizeRef(extraProps) : vnode.ref,
455+
ref:
456+
extraProps && extraProps.ref
457+
? // #2078 in the case of <component :is="vnode" ref="extra"/>
458+
// if the vnode itself already has a ref, cloneVNode will need to merge
459+
// the refs so the single vnode can be set on multiple refs
460+
mergeRef && ref
461+
? isArray(ref)
462+
? ref.concat(normalizeRef(extraProps)!)
463+
: [ref, normalizeRef(extraProps)!]
464+
: normalizeRef(extraProps)
465+
: ref,
445466
scopeId: vnode.scopeId,
446467
children: vnode.children,
447468
target: vnode.target,

0 commit comments

Comments
 (0)