diff --git a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts index a7ae7a06bfd..ae30878a4fc 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,51 @@ 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) + }) + test('string ref inside slots', async () => { const root = nodeOps.createElement('div') const spy = vi.fn() diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts index ca21030dc35..0e40c9534ca 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 */ @@ -153,9 +154,19 @@ 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(vnode) + } + job.id = -1 + pendingSetRefMap.set(vnode, job) + queuePostRenderEffect(job, parentSuspense) } else { + const pendingSetRef = pendingSetRefMap.get(vnode) + if (pendingSetRef) { + pendingSetRef.flags! |= SchedulerJobFlags.DISPOSED + pendingSetRefMap.delete(vnode) + } doSet() } } else if (__DEV__) {