Skip to content

Commit 93ba107

Browse files
authored
fix(templateRef): prevent unnecessary set ref on dynamic ref change or component unmount (#12642)
close #12639
1 parent 00978f7 commit 93ba107

File tree

2 files changed

+103
-3
lines changed

2 files changed

+103
-3
lines changed

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
render,
1111
serializeInner,
1212
shallowRef,
13+
watch,
1314
} from '@vue/runtime-test'
1415

1516
describe('api: template refs', () => {
@@ -179,6 +180,89 @@ describe('api: template refs', () => {
179180
expect(el.value).toBe(null)
180181
})
181182

183+
// #12639
184+
it('update and unmount child in the same tick', async () => {
185+
const root = nodeOps.createElement('div')
186+
const el = ref(null)
187+
const toggle = ref(true)
188+
const show = ref(true)
189+
190+
const Comp = defineComponent({
191+
emits: ['change'],
192+
props: ['show'],
193+
setup(props, { emit }) {
194+
watch(
195+
() => props.show,
196+
() => {
197+
emit('change')
198+
},
199+
)
200+
return () => h('div', 'hi')
201+
},
202+
})
203+
204+
const App = {
205+
setup() {
206+
return {
207+
refKey: el,
208+
}
209+
},
210+
render() {
211+
return toggle.value
212+
? h(Comp, {
213+
ref: 'refKey',
214+
show: show.value,
215+
onChange: () => (toggle.value = false),
216+
})
217+
: null
218+
},
219+
}
220+
render(h(App), root)
221+
expect(el.value).not.toBe(null)
222+
223+
show.value = false
224+
await nextTick()
225+
expect(el.value).toBe(null)
226+
})
227+
228+
it('set and change ref in the same tick', async () => {
229+
const root = nodeOps.createElement('div')
230+
const show = ref(false)
231+
const refName = ref('a')
232+
233+
const Child = defineComponent({
234+
setup() {
235+
refName.value = 'b'
236+
return () => {}
237+
},
238+
})
239+
240+
const Comp = {
241+
render() {
242+
return h(Child, {
243+
ref: refName.value,
244+
})
245+
},
246+
updated(this: any) {
247+
expect(this.$refs.a).toBe(null)
248+
expect(this.$refs.b).not.toBe(null)
249+
},
250+
}
251+
252+
const App = {
253+
render() {
254+
return show.value ? h(Comp) : null
255+
},
256+
}
257+
258+
render(h(App), root)
259+
expect(refName.value).toBe('a')
260+
261+
show.value = true
262+
await nextTick()
263+
expect(refName.value).toBe('b')
264+
})
265+
182266
it('unset old ref when new ref is absent', async () => {
183267
const root1 = nodeOps.createElement('div')
184268
const root2 = nodeOps.createElement('div')

packages/runtime-core/src/rendererTemplateRef.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import { isAsyncWrapper } from './apiAsyncComponent'
1919
import { warn } from './warning'
2020
import { isRef, toRaw } from '@vue/reactivity'
2121
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
22-
import type { SchedulerJob } from './scheduler'
22+
import { type SchedulerJob, SchedulerJobFlags } from './scheduler'
2323
import { queuePostRenderEffect } from './renderer'
2424
import { type ComponentOptions, getComponentPublicInstance } from './component'
2525
import { knownTemplateRefs } from './helpers/useTemplateRef'
2626

27+
const pendingSetRefMap = new WeakMap<VNodeNormalizedRef, SchedulerJob>()
2728
/**
2829
* Function for handling a template ref
2930
*/
@@ -106,6 +107,7 @@ export function setRef(
106107

107108
// dynamic ref changed. unset old ref
108109
if (oldRef != null && oldRef !== ref) {
110+
invalidatePendingSetRef(oldRawRef!)
109111
if (isString(oldRef)) {
110112
refs[oldRef] = null
111113
if (canSetSetupRef(oldRef)) {
@@ -176,13 +178,27 @@ export function setRef(
176178
// #1789: for non-null values, set them after render
177179
// null values means this is unmount and it should not overwrite another
178180
// ref with the same key
179-
;(doSet as SchedulerJob).id = -1
180-
queuePostRenderEffect(doSet, parentSuspense)
181+
const job: SchedulerJob = () => {
182+
doSet()
183+
pendingSetRefMap.delete(rawRef)
184+
}
185+
job.id = -1
186+
pendingSetRefMap.set(rawRef, job)
187+
queuePostRenderEffect(job, parentSuspense)
181188
} else {
189+
invalidatePendingSetRef(rawRef)
182190
doSet()
183191
}
184192
} else if (__DEV__) {
185193
warn('Invalid template ref type:', ref, `(${typeof ref})`)
186194
}
187195
}
188196
}
197+
198+
function invalidatePendingSetRef(rawRef: VNodeNormalizedRef) {
199+
const pendingSetRef = pendingSetRefMap.get(rawRef)
200+
if (pendingSetRef) {
201+
pendingSetRef.flags! |= SchedulerJobFlags.DISPOSED
202+
pendingSetRefMap.delete(rawRef)
203+
}
204+
}

0 commit comments

Comments
 (0)