Skip to content

Commit 750e7cd

Browse files
authored
fix(templateRef): prevent duplicate onScopeDispose registrations for dynamic template refs (#14161)
1 parent 0668ea3 commit 750e7cd

File tree

2 files changed

+107
-10
lines changed

2 files changed

+107
-10
lines changed

packages/runtime-vapor/__tests__/dom/templateRef.spec.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ describe('api: template ref', () => {
182182
expect(fn.mock.calls[0][0]).toBe(host.children[0])
183183
toggle.value = false
184184
await nextTick()
185-
expect(fn.mock.calls[1][0]).toBe(undefined)
185+
expect(fn.mock.calls[1][0]).toBe(null)
186186
})
187187

188188
test('useTemplateRef mount', () => {
@@ -756,6 +756,90 @@ describe('api: template ref', () => {
756756
}
757757
})
758758

759+
it('should not register duplicate onScopeDispose callbacks for dynamic function refs', async () => {
760+
const fn1 = vi.fn()
761+
const fn2 = vi.fn()
762+
const toggle = ref(true)
763+
const t0 = template('<div></div>')
764+
765+
const { app } = define({
766+
render() {
767+
const n0 = t0()
768+
let r0: any
769+
renderEffect(() => {
770+
r0 = createTemplateRefSetter()(
771+
n0 as Element,
772+
toggle.value ? fn1 : fn2,
773+
r0,
774+
)
775+
})
776+
return n0
777+
},
778+
}).render()
779+
780+
expect(fn1).toHaveBeenCalledTimes(1)
781+
expect(fn2).toHaveBeenCalledTimes(0)
782+
expect(app._instance!.scope.cleanups.length).toBe(1)
783+
784+
toggle.value = false
785+
await nextTick()
786+
expect(fn1).toHaveBeenCalledTimes(1)
787+
expect(fn2).toHaveBeenCalledTimes(1)
788+
expect(app._instance!.scope.cleanups.length).toBe(1)
789+
790+
toggle.value = true
791+
await nextTick()
792+
expect(fn1).toHaveBeenCalledTimes(2)
793+
expect(fn2).toHaveBeenCalledTimes(1)
794+
expect(app._instance!.scope.cleanups.length).toBe(1)
795+
796+
app.unmount()
797+
await nextTick()
798+
// expected fn1 to be called again during scope dispose
799+
expect(fn1).toHaveBeenCalledTimes(3)
800+
expect(fn2).toHaveBeenCalledTimes(1)
801+
})
802+
803+
it('should not register duplicate onScopeDispose callbacks for dynamic string refs', async () => {
804+
const el1 = ref(null)
805+
const el2 = ref(null)
806+
const toggle = ref(true)
807+
const t0 = template('<div></div>')
808+
809+
const { app, host } = define({
810+
setup() {
811+
return { ref1: el1, ref2: el2 }
812+
},
813+
render() {
814+
const n0 = t0()
815+
let r0: any
816+
renderEffect(() => {
817+
r0 = createTemplateRefSetter()(
818+
n0 as Element,
819+
toggle.value ? 'ref1' : 'ref2',
820+
r0,
821+
)
822+
})
823+
return n0
824+
},
825+
}).render()
826+
827+
expect(el1.value).toBe(host.children[0])
828+
expect(el2.value).toBe(null)
829+
expect(app._instance!.scope.cleanups.length).toBe(1)
830+
831+
toggle.value = false
832+
await nextTick()
833+
expect(el1.value).toBe(null)
834+
expect(el2.value).toBe(host.children[0])
835+
expect(app._instance!.scope.cleanups.length).toBe(1)
836+
837+
app.unmount()
838+
await nextTick()
839+
expect(el1.value).toBe(null)
840+
expect(el2.value).toBe(null)
841+
})
842+
759843
// TODO: can not reproduce in Vapor
760844
// // #2078
761845
// test('handling multiple merged refs', async () => {

packages/runtime-vapor/src/apiTemplateRef.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
} from '@vue/runtime-dom'
1717
import {
1818
EMPTY_OBJ,
19-
hasOwn,
19+
NO,
20+
NOOP,
2021
isArray,
2122
isFunction,
2223
isString,
@@ -38,6 +39,20 @@ export type setRefFn = (
3839
refKey?: string,
3940
) => NodeRef | undefined
4041

42+
const refCleanups = new WeakMap<RefEl, { fn: () => void }>()
43+
44+
function ensureCleanup(el: RefEl): { fn: () => void } {
45+
let cleanupRef = refCleanups.get(el)
46+
if (!cleanupRef) {
47+
refCleanups.set(el, (cleanupRef = { fn: NOOP }))
48+
onScopeDispose(() => {
49+
cleanupRef!.fn()
50+
refCleanups.delete(el)
51+
})
52+
}
53+
return cleanupRef
54+
}
55+
4156
export function createTemplateRefSetter(): setRefFn {
4257
const instance = currentInstance as VaporComponentInstance
4358
return (...args) => setRef(instance, ...args)
@@ -80,12 +95,12 @@ export function setRef(
8095
const refs =
8196
instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs
8297

83-
const canSetSetupRef = createCanSetSetupRefChecker(setupState)
98+
const canSetSetupRef = __DEV__ ? createCanSetSetupRefChecker(setupState) : NO
8499
// dynamic ref changed. unset old ref
85100
if (oldRef != null && oldRef !== ref) {
86101
if (isString(oldRef)) {
87102
refs[oldRef] = null
88-
if (__DEV__ && hasOwn(setupState, oldRef)) {
103+
if (__DEV__ && canSetSetupRef(oldRef)) {
89104
setupState[oldRef] = null
90105
}
91106
} else if (isRef(oldRef)) {
@@ -94,16 +109,15 @@ export function setRef(
94109
}
95110

96111
if (isFunction(ref)) {
97-
const invokeRefSetter = (value?: Element | Record<string, any>) => {
112+
const invokeRefSetter = (value?: Element | Record<string, any> | null) => {
98113
callWithErrorHandling(ref, currentInstance, ErrorCodes.FUNCTION_REF, [
99114
value,
100115
refs,
101116
])
102117
}
103118

104119
invokeRefSetter(refValue)
105-
// TODO this gets called repeatedly in renderEffect when it's dynamic ref?
106-
onScopeDispose(() => invokeRefSetter())
120+
ensureCleanup(el).fn = () => invokeRefSetter(null)
107121
} else {
108122
const _isString = isString(ref)
109123
const _isRef = isRef(ref)
@@ -150,8 +164,7 @@ export function setRef(
150164
}
151165
queuePostFlushCb(doSet, -1)
152166

153-
// TODO this gets called repeatedly in renderEffect when it's dynamic ref?
154-
onScopeDispose(() => {
167+
ensureCleanup(el).fn = () => {
155168
queuePostFlushCb(() => {
156169
if (isArray(existing)) {
157170
remove(existing, refValue)
@@ -165,7 +178,7 @@ export function setRef(
165178
if (refKey) refs[refKey] = null
166179
}
167180
})
168-
})
181+
}
169182
} else if (__DEV__) {
170183
warn('Invalid template ref type:', ref, `(${typeof ref})`)
171184
}

0 commit comments

Comments
 (0)