diff --git a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts index 7803826e3..53e03c827 100644 --- a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts +++ b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts @@ -10,6 +10,7 @@ import { render, serializeInner, shallowRef, + watch, } from '@vue/runtime-test' describe('api: template refs', () => { @@ -179,6 +180,89 @@ describe('api: template refs', () => { expect(el.value).toBe(null) }) + // #12639 + it('update and unmount child in the same tick', async () => { + const root = nodeOps.createElement('div') + const el = ref(null) + const toggle = ref(true) + const show = ref(true) + + const Comp = defineComponent({ + emits: ['change'], + props: ['show'], + setup(props, { emit }) { + watch( + () => props.show, + () => { + emit('change') + }, + ) + return () => h('div', 'hi') + }, + }) + + const App = { + setup() { + return { + refKey: el, + } + }, + render() { + return toggle.value + ? h(Comp, { + ref: 'refKey', + show: show.value, + onChange: () => (toggle.value = false), + }) + : null + }, + } + render(h(App), root) + expect(el.value).not.toBe(null) + + show.value = false + await nextTick() + expect(el.value).toBe(null) + }) + + it('set and change ref in the same tick', async () => { + const root = nodeOps.createElement('div') + const show = ref(false) + const refName = ref('a') + + const Child = defineComponent({ + setup() { + refName.value = 'b' + return () => {} + }, + }) + + const Comp = { + render() { + return h(Child, { + ref: refName.value, + }) + }, + updated(this: any) { + expect(this.$refs.a).toBe(null) + expect(this.$refs.b).not.toBe(null) + }, + } + + const App = { + render() { + return show.value ? h(Comp) : null + }, + } + + render(h(App), root) + expect(refName.value).toBe('a') + + show.value = true + await nextTick() + expect(refName.value).toBe('b') + }) + it('unset old ref when new ref is absent', async () => { const root1 = nodeOps.createElement('div') const root2 = nodeOps.createElement('div') diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts index ca21030dc..0241aa528 100644 --- a/packages/runtime-core/src/rendererTemplateRef.ts +++ b/packages/runtime-core/src/rendererTemplateRef.ts @@ -13,11 +13,12 @@ import { isAsyncWrapper } from './apiAsyncComponent' import { warn } from './warning' import { isRef, toRaw } from '@vue/reactivity' import { ErrorCodes, callWithErrorHandling } from './errorHandling' -import type { SchedulerJob } from './scheduler' +import { type SchedulerJob, SchedulerJobFlags } from './scheduler' import { queuePostRenderEffect } from './renderer' import { type ComponentOptions, getComponentPublicInstance } from './component' import { knownTemplateRefs } from './helpers/useTemplateRef' +const pendingSetRefMap = new WeakMap() /** * Function for handling a template ref */ @@ -96,6 +97,7 @@ export function setRef( // dynamic ref changed. unset old ref if (oldRef != null && oldRef !== ref) { + invalidatePendingSetRef(oldRawRef!) if (isString(oldRef)) { refs[oldRef] = null if (canSetSetupRef(oldRef)) { @@ -153,9 +155,15 @@ export function setRef( // #1789: for non-null values, set them after render // null values means this is unmount and it should not overwrite another // ref with the same key - ;(doSet as SchedulerJob).id = -1 - queuePostRenderEffect(doSet, parentSuspense) + const job: SchedulerJob = () => { + doSet() + pendingSetRefMap.delete(rawRef) + } + job.id = -1 + pendingSetRefMap.set(rawRef, job) + queuePostRenderEffect(job, parentSuspense) } else { + invalidatePendingSetRef(rawRef) doSet() } } else if (__DEV__) { @@ -163,3 +171,11 @@ export function setRef( } } } + +function invalidatePendingSetRef(rawRef: VNodeNormalizedRef) { + const pendingSetRef = pendingSetRefMap.get(rawRef) + if (pendingSetRef) { + pendingSetRef.flags! |= SchedulerJobFlags.DISPOSED + pendingSetRefMap.delete(rawRef) + } +}