Skip to content

Commit

Permalink
fix(runtime-core/refs): handle multiple merged refs for dynamic compo…
Browse files Browse the repository at this point in the history
…nent with vnode

fix #2078
  • Loading branch information
yyx990803 committed Sep 14, 2020
1 parent 313dd06 commit 612eb67
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 11 deletions.
42 changes: 41 additions & 1 deletion packages/runtime-core/__tests__/apiTemplateRef.spec.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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)
})
})
20 changes: 17 additions & 3 deletions packages/runtime-core/src/renderer.ts
Expand Up @@ -10,7 +10,8 @@ import {
isSameVNodeType,
Static,
VNodeNormalizedRef,
VNodeHook
VNodeHook,
VNodeNormalizedRefAtom
} from './vnode'
import {
ComponentInternalInstance,
Expand Down Expand Up @@ -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
Expand All @@ -295,15 +309,15 @@ 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. ` +
`A vnode with ref must be created inside the render function.`
)
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

Expand Down
35 changes: 28 additions & 7 deletions packages/runtime-core/src/vnode.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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)
}
Expand Down Expand Up @@ -429,19 +439,30 @@ 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,
[ReactiveFlags.SKIP]: true,
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,
Expand Down

0 comments on commit 612eb67

Please sign in to comment.