fix(async-component): forward refs on async component wrapper

fix #2671
This commit is contained in:
Evan You 2020-11-30 18:59:14 -05:00
parent 87581cd266
commit 64d4681e4b
4 changed files with 66 additions and 18 deletions

View File

@ -652,4 +652,49 @@ describe('api: defineAsyncComponent', () => {
expect(loaderCallCount).toBe(2) expect(loaderCallCount).toBe(2)
expect(serializeInner(root)).toBe('<!---->') expect(serializeInner(root)).toBe('<!---->')
}) })
test('template ref forwarding', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent(
() =>
new Promise(r => {
resolve = r as any
})
)
const fooRef = ref()
const toggle = ref(true)
const root = nodeOps.createElement('div')
createApp({
render: () => (toggle.value ? h(Foo, { ref: fooRef }) : null)
}).mount(root)
expect(serializeInner(root)).toBe('<!---->')
expect(fooRef.value).toBe(null)
resolve!({
data() {
return {
id: 'foo'
}
},
render: () => 'resolved'
})
// first time resolve, wait for macro task since there are multiple
// microtasks / .then() calls
await timeout()
expect(serializeInner(root)).toBe('resolved')
expect(fooRef.value.id).toBe('foo')
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
expect(fooRef.value).toBe(null)
// already resolved component should update on nextTick
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe('resolved')
expect(fooRef.value.id).toBe('foo')
})
}) })

View File

@ -3,11 +3,12 @@ import {
ConcreteComponent, ConcreteComponent,
currentInstance, currentInstance,
ComponentInternalInstance, ComponentInternalInstance,
isInSSRComponentSetup isInSSRComponentSetup,
ComponentOptions
} from './component' } from './component'
import { isFunction, isObject } from '@vue/shared' import { isFunction, isObject } from '@vue/shared'
import { ComponentPublicInstance } from './componentPublicInstance' import { ComponentPublicInstance } from './componentPublicInstance'
import { createVNode } from './vnode' import { createVNode, VNode } from './vnode'
import { defineComponent } from './apiDefineComponent' import { defineComponent } from './apiDefineComponent'
import { warn } from './warning' import { warn } from './warning'
import { ref } from '@vue/reactivity' import { ref } from '@vue/reactivity'
@ -34,6 +35,9 @@ export interface AsyncComponentOptions<T = any> {
) => any ) => any
} }
export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean =>
!!(i.type as ComponentOptions).__asyncLoader
export function defineAsyncComponent< export function defineAsyncComponent<
T extends Component = { new (): ComponentPublicInstance } T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T { >(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
@ -193,7 +197,10 @@ export function defineAsyncComponent<
function createInnerComp( function createInnerComp(
comp: ConcreteComponent, comp: ConcreteComponent,
{ vnode: { props, children } }: ComponentInternalInstance { vnode: { ref, props, children } }: ComponentInternalInstance
) { ) {
return createVNode(comp, props, children) const vnode = createVNode(comp, props, children)
// ensure inner component inherits the async wrapper's ref owner
vnode.ref = ref
return vnode
} }

View File

@ -74,6 +74,7 @@ import { startMeasure, endMeasure } from './profiling'
import { ComponentPublicInstance } from './componentPublicInstance' import { ComponentPublicInstance } from './componentPublicInstance'
import { devtoolsComponentRemoved, devtoolsComponentUpdated } from './devtools' import { devtoolsComponentRemoved, devtoolsComponentUpdated } from './devtools'
import { initFeatureFlags } from './featureFlags' import { initFeatureFlags } from './featureFlags'
import { isAsyncWrapper } from './apiAsyncComponent'
export interface Renderer<HostElement = RendererElement> { export interface Renderer<HostElement = RendererElement> {
render: RootRenderFunction<HostElement> render: RootRenderFunction<HostElement>
@ -289,7 +290,6 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__
export const setRef = ( export const setRef = (
rawRef: VNodeNormalizedRef, rawRef: VNodeNormalizedRef,
oldRawRef: VNodeNormalizedRef | null, oldRawRef: VNodeNormalizedRef | null,
parentComponent: ComponentInternalInstance,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
vnode: VNode | null vnode: VNode | null
) => { ) => {
@ -298,7 +298,6 @@ export const setRef = (
setRef( setRef(
r, r,
oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
parentComponent,
parentSuspense, parentSuspense,
vnode vnode
) )
@ -307,7 +306,7 @@ export const setRef = (
} }
let value: ComponentPublicInstance | RendererNode | Record<string, any> | null let value: ComponentPublicInstance | RendererNode | Record<string, any> | null
if (!vnode) { if (!vnode || isAsyncWrapper(vnode)) {
value = null value = null
} else { } else {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
@ -368,10 +367,7 @@ export const setRef = (
doSet() doSet()
} }
} else if (isFunction(ref)) { } else if (isFunction(ref)) {
callWithErrorHandling(ref, parentComponent, ErrorCodes.FUNCTION_REF, [ callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
value,
refs
])
} else if (__DEV__) { } else if (__DEV__) {
warn('Invalid template ref type:', value, `(${typeof value})`) warn('Invalid template ref type:', value, `(${typeof value})`)
} }
@ -552,7 +548,7 @@ function baseCreateRenderer(
// set ref // set ref
if (ref != null && parentComponent) { if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2) setRef(ref, n1 && n1.ref, parentSuspense, n2)
} }
} }
@ -1983,8 +1979,8 @@ function baseCreateRenderer(
dirs dirs
} = vnode } = vnode
// unset ref // unset ref
if (ref != null && parentComponent) { if (ref != null) {
setRef(ref, null, parentComponent, parentSuspense, null) setRef(ref, null, parentSuspense, null)
} }
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {

View File

@ -21,7 +21,7 @@ import {
isClassComponent isClassComponent
} from './component' } from './component'
import { RawSlots } from './componentSlots' import { RawSlots } from './componentSlots'
import { isProxy, Ref, toRaw, ReactiveFlags } from '@vue/reactivity' import { isProxy, Ref, toRaw, ReactiveFlags, isRef } from '@vue/reactivity'
import { AppContext } from './apiCreateApp' import { AppContext } from './apiCreateApp'
import { import {
SuspenseImpl, SuspenseImpl,
@ -304,9 +304,9 @@ const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => { const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
return (ref != null return (ref != null
? isArray(ref) ? isString(ref) || isRef(ref) || isFunction(ref)
? ref ? { i: currentRenderingInstance, r: ref }
: { i: currentRenderingInstance, r: ref } : ref
: null) as any : null) as any
} }