Template Refs
Consider the following component:
<script setup>
import { ref, onMounted } from "vue";
const divRef = ref(null);
onMounted(() => {
console.log(divRef.value); // <div>...</div>
});
</script>
<template>
<div ref="divRef">Hello, Template Refs!</div>
</template>Compilation Result and Overview
Static ref
<template>
<div ref="foo">content</div>
</template>The compilation result is as follows:
import { setRef as _setRef, template as _template } from "vue/vapor";
const t0 = _template("<div>content</div>");
function _sfc_render(_ctx) {
const n0 = t0();
_setRef(n0, "foo");
return n0;
}setRef is a helper function that sets a ref on an element.
Dynamic ref
<template>
<div :ref="foo">content</div>
</template>The compilation result is as follows:
import {
renderEffect as _renderEffect,
setRef as _setRef,
template as _template,
} from "vue/vapor";
const t0 = _template("<div>content</div>");
function _sfc_render(_ctx) {
const n0 = t0();
let r0;
_renderEffect(() => (r0 = _setRef(n0, _ctx.foo, r0)));
return n0;
}For dynamic refs, setRef is called within renderEffect, passing the previous ref value (r0) to unset the old ref.
ref + v-for
<template>
<div v-for="i in [1, 2, 3]" ref="foo">{{ i }}</div>
</template>The compilation result is as follows:
import {
setRef as _setRef,
createFor as _createFor,
template as _template,
} from "vue/vapor";
const t0 = _template("<div></div>");
function _sfc_render(_ctx) {
const n0 = _createFor(
() => [1, 2, 3],
(_ctx0) => {
const n2 = t0();
_setRef(n2, "foo", void 0, true);
return n2;
}
);
return n0;
}For refs within v-for, the 4th argument refFor is set to true, managing the ref as an array.
ref + v-if
<template>
<div v-if="true" ref="foo">content</div>
</template>The compilation result is as follows:
import {
setRef as _setRef,
createIf as _createIf,
template as _template,
} from "vue/vapor";
const t0 = _template("<div>content</div>");
function _sfc_render(_ctx) {
const n0 = _createIf(
() => true,
() => {
const n2 = t0();
_setRef(n2, "foo");
return n2;
}
);
return n0;
}For refs within v-if, they are set normally but are automatically cleaned up when the block's scope is destroyed.
Reading the Compiler
IR
Let's look at the SET_TEMPLATE_REF IR.
140export interface SetTemplateRefIRNode extends BaseIRNode {
141 type: IRNodeTypes.SET_TEMPLATE_REF
142 element: number
143 value: SimpleExpressionNode
144 refFor: boolean
145 effect: boolean
146}export interface SetTemplateRefIRNode extends BaseIRNode {
type: IRNodeTypes.SET_TEMPLATE_REF;
element: number;
value: SimpleExpressionNode;
refFor: boolean;
effect: boolean;
}element: The ID of the element to set the ref onvalue: The ref name or expressionrefFor: Whether inside v-foreffect: Whether it's a dynamic ref
Transformer
12export const transformTemplateRef: NodeTransform = (node, context) => {
13 if (node.type !== NodeTypes.ELEMENT) return
14
15 const dir = findProp(node, 'ref', false, true)
16 if (!dir) return
17
18 let value: SimpleExpressionNode
19 if (dir.type === NodeTypes.DIRECTIVE) {
20 value = dir.exp || normalizeBindShorthand(dir.arg!, context)
21 } else {
22 value = dir.value
23 ? createSimpleExpression(dir.value.content, true, dir.value.loc)
24 : EMPTY_EXPRESSION
25 }
26
27 return () => {
28 const id = context.reference()
29 const effect = !isConstantExpression(value)
30 effect &&
31 context.registerOperation({
32 type: IRNodeTypes.DECLARE_OLD_REF,
33 id,
34 })
35 context.registerEffect([value], {
36 type: IRNodeTypes.SET_TEMPLATE_REF,
37 element: id,
38 value,
39 refFor: !!context.inVFor,
40 effect,
41 })
42 }
43}export const transformTemplateRef: NodeTransform = (node, context) => {
if (node.type !== NodeTypes.ELEMENT) return;
const dir = findProp(node, "ref", false, true);
if (!dir) return;
let value: SimpleExpressionNode;
if (dir.type === NodeTypes.DIRECTIVE) {
// For :ref="foo"
value = dir.exp || normalizeBindShorthand(dir.arg!, context);
} else {
// For ref="foo"
value = dir.value
? createSimpleExpression(dir.value.content, true, dir.value.loc)
: EMPTY_EXPRESSION;
}
return () => {
const id = context.reference();
const effect = !isConstantExpression(value);
// For dynamic refs, declare a variable to track the old ref
effect &&
context.registerOperation({
type: IRNodeTypes.DECLARE_OLD_REF,
id,
});
context.registerEffect([value], {
type: IRNodeTypes.SET_TEMPLATE_REF,
element: id,
value,
refFor: !!context.inVFor,
effect,
});
};
};Key points:
- Detect
refattribute or:refdirective - Determine if static or dynamic (
isConstantExpression) - For dynamic, declare a variable to track the old ref with
DECLARE_OLD_REF - Register the ref setting with
SET_TEMPLATE_REF
Reading the Runtime
setRef is implemented in dom/templateRef.ts in runtime-vapor.
30export function setRef(
31 el: RefEl,
32 ref: NodeRef,
33 oldRef?: NodeRef,
34 refFor = false,
35): NodeRef | undefined {
36 if (!currentInstance) return
37 const { setupState, isUnmounted } = currentInstance
38
39 if (isUnmounted) {
40 return
41 }
42
43 const refValue = isVaporComponent(el) ? el.exposed || el : el
44
45 const refs =
46 currentInstance.refs === EMPTY_OBJ
47 ? (currentInstance.refs = {})
48 : currentInstance.refs
49
50 // dynamic ref changed. unset old ref
51 if (oldRef != null && oldRef !== ref) {
52 if (isString(oldRef)) {
53 refs[oldRef] = null
54 if (hasOwn(setupState, oldRef)) {
55 setupState[oldRef] = null
56 }
57 } else if (isRef(oldRef)) {
58 oldRef.value = null
59 }
60 }
61
62 if (isFunction(ref)) {
63 const invokeRefSetter = (value?: Element | Record<string, any>) => {
64 callWithErrorHandling(
65 ref,
66 currentInstance,
67 VaporErrorCodes.FUNCTION_REF,
68 [value, refs],
69 )
70 }
71
72 invokeRefSetter(refValue)
73 onScopeDispose(() => invokeRefSetter())
74 } else {
75 const _isString = isString(ref)
76 const _isRef = isRef(ref)
77 let existing: unknown
78
79 if (_isString || _isRef) {
80 const doSet: SchedulerJob = () => {
81 if (refFor) {
82 existing = _isString
83 ? hasOwn(setupState, ref)
84 ? setupState[ref]
85 : refs[ref]
86 : ref.value
87
88 if (!isArray(existing)) {
89 existing = [refValue]
90 if (_isString) {
91 refs[ref] = existing
92 if (hasOwn(setupState, ref)) {
93 setupState[ref] = refs[ref]
94 // if setupState[ref] is a reactivity ref,
95 // the existing will also become reactivity too
96 // need to get the Proxy object by resetting
97 existing = setupState[ref]
98 }
99 } else {
100 ref.value = existing
101 }
102 } else if (!existing.includes(refValue)) {
103 existing.push(refValue)
104 }
105 } else if (_isString) {
106 refs[ref] = refValue
107 if (hasOwn(setupState, ref)) {
108 setupState[ref] = refValue
109 }
110 } else if (_isRef) {
111 ref.value = refValue
112 } else if (__DEV__) {
113 warn('Invalid template ref type:', ref, `(${typeof ref})`)
114 }
115 }
116 doSet.id = -1
117 queuePostFlushCb(doSet)
118
119 onScopeDispose(() => {
120 queuePostFlushCb(() => {
121 if (isArray(existing)) {
122 remove(existing, refValue)
123 } else if (_isString) {
124 refs[ref] = null
125 if (hasOwn(setupState, ref)) {
126 setupState[ref] = null
127 }
128 } else if (_isRef) {
129 ref.value = null
130 }
131 })
132 })
133 } else if (__DEV__) {
134 warn('Invalid template ref type:', ref, `(${typeof ref})`)
135 }
136 }
137 return ref
138}export function setRef(
el: RefEl,
ref: NodeRef,
oldRef?: NodeRef,
refFor = false
): NodeRef | undefined {
if (!currentInstance) return;
const { setupState, isUnmounted } = currentInstance;
if (isUnmounted) {
return;
}
const refValue = isVaporComponent(el) ? el.exposed || el : el;
const refs =
currentInstance.refs === EMPTY_OBJ
? (currentInstance.refs = {})
: currentInstance.refs;
// If dynamic ref changed, unset the old ref
if (oldRef != null && oldRef !== ref) {
if (isString(oldRef)) {
refs[oldRef] = null;
if (hasOwn(setupState, oldRef)) {
setupState[oldRef] = null;
}
} else if (isRef(oldRef)) {
oldRef.value = null;
}
}
// ...
}Types of Refs
setRef supports 3 types of refs:
- String ref:
ref="foo"- Set tosetupState.fooorrefs.foo - Ref object:
:ref="myRef"- Set tomyRef.value - Function ref:
:ref="(el) => ..."- Calls the function
Array refs in v-for
When refFor = true, refs are managed as an array:
if (refFor) {
existing = _isString
? hasOwn(setupState, ref)
? setupState[ref]
: refs[ref]
: ref.value;
if (!isArray(existing)) {
existing = [refValue];
// ...
} else if (!existing.includes(refValue)) {
existing.push(refValue);
}
}Cleanup
Using onScopeDispose, refs are cleaned up when the scope is destroyed:
onScopeDispose(() => {
queuePostFlushCb(() => {
if (isArray(existing)) {
remove(existing, refValue);
} else if (_isString) {
refs[ref] = null;
if (hasOwn(setupState, ref)) {
setupState[ref] = null;
}
} else if (_isRef) {
ref.value = null;
}
});
});This ensures that refs are automatically cleaned up when blocks are destroyed in v-if or v-for.
queuePostFlushCb
Ref setting is registered to the Post Flush Queue using queuePostFlushCb:
const doSet: SchedulerJob = () => {
// ref setting logic
};
doSet.id = -1;
queuePostFlushCb(doSet);id = -1 ensures execution before other jobs.
This means refs are set after DOM updates but before other effects.
Template Refs in Vapor Mode provide the same behavior as the Options API.
It supports all use cases including dynamic refs, array refs in v-for, and function refs.
By utilizing onScopeDispose, proper cleanup is also guaranteed.
