]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(templateRef): prevent unnecessary set ref on dynamic ref change or component...
authoredison <daiwei521@126.com>
Tue, 2 Sep 2025 09:08:53 +0000 (17:08 +0800)
committerGitHub <noreply@github.com>
Tue, 2 Sep 2025 09:08:53 +0000 (17:08 +0800)
close #12639

packages/runtime-core/__tests__/rendererTemplateRef.spec.ts
packages/runtime-core/src/rendererTemplateRef.ts

index 7803826e3df2d37d28814bb9588c7a0c574cbce8..53e03c827b97dff72324947e30f1e5781b918e55 100644 (file)
@@ -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')
index bcd7d83a0a7910ea4577f6234701dafab0ce9043..d39e6215ff36b5bf9337a46f65b7f1fa81d71a5e 100644 (file)
@@ -19,11 +19,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<VNodeNormalizedRef, SchedulerJob>()
 /**
  * Function for handling a template ref
  */
@@ -106,6 +107,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)) {
@@ -176,9 +178,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__) {
@@ -186,3 +194,11 @@ export function setRef(
     }
   }
 }
+
+function invalidatePendingSetRef(rawRef: VNodeNormalizedRef) {
+  const pendingSetRef = pendingSetRefMap.get(rawRef)
+  if (pendingSetRef) {
+    pendingSetRef.flags! |= SchedulerJobFlags.DISPOSED
+    pendingSetRefMap.delete(rawRef)
+  }
+}