Template Refs
以下のようなコンポーネントを考えます.
<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>コンパイル結果と概要
静的な ref
<template>
<div ref="foo">content</div>
</template>コンパイル結果は以下のようになります.
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 は要素に対して ref を設定するヘルパー関数です.
動的な ref
<template>
<div :ref="foo">content</div>
</template>コンパイル結果は以下のようになります.
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;
}動的な ref の場合は renderEffect 内で setRef を呼び出し,前の ref 値 (r0) を渡して古い ref を解除しています.
ref + v-for
<template>
<div v-for="i in [1, 2, 3]" ref="foo">{{ i }}</div>
</template>コンパイル結果は以下のようになります.
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;
}v-for 内の ref は第 4 引数 refFor を true にすることで,配列として ref を管理します.
ref + v-if
<template>
<div v-if="true" ref="foo">content</div>
</template>コンパイル結果は以下のようになります.
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;
}v-if 内の ref は通常通り設定されますが,ブロックのスコープ破棄時に自動的にクリーンアップされます.
コンパイラを読む
IR
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: ref を設定する要素の IDvalue: ref の名前または式refFor: v-for 内かどうかeffect: 動的な 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) {
// :ref="foo" の場合
value = dir.exp || normalizeBindShorthand(dir.arg!, context);
} else {
// ref="foo" の場合
value = dir.value
? createSimpleExpression(dir.value.content, true, dir.value.loc)
: EMPTY_EXPRESSION;
}
return () => {
const id = context.reference();
const effect = !isConstantExpression(value);
// 動的な ref の場合は古い 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,
});
};
};ポイント:
ref属性または:refディレクティブを検出- 静的か動的かを判定 (
isConstantExpression) - 動的な場合は
DECLARE_OLD_REFで古い ref を追跡する変数を宣言 SET_TEMPLATE_REFで ref 設定を登録
ランタイムを読む
setRef は runtime-vapor の dom/templateRef.ts に実装されています.
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;
// 動的な ref が変更された場合,古い 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;
}
}
// ...
}ref の種類
setRef は 3 種類の ref をサポートしています:
- 文字列 ref:
ref="foo"-setupState.fooまたはrefs.fooに設定 - Ref オブジェクト:
:ref="myRef"-myRef.valueに設定 - 関数 ref:
:ref="(el) => ..."- 関数を呼び出し
v-for での配列 ref
refFor = true の場合,ref は配列として管理されます:
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);
}
}クリーンアップ
onScopeDispose を使って,スコープ破棄時に ref をクリーンアップします:
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;
}
});
});これにより,v-if や v-for でブロックが破棄された際に,自動的に ref がクリーンアップされます.
queuePostFlushCb
ref の設定は queuePostFlushCb を使って Post Flush Queue に登録されます:
const doSet: SchedulerJob = () => {
// ref 設定ロジック
};
doSet.id = -1;
queuePostFlushCb(doSet);id = -1 は他のジョブより先に実行されることを保証します.
これにより,ref は DOM 更新後,他の effect より前に設定されます.
Template Refs は Vapor Mode でも Options API と同様の動作を提供します.
動的な ref,v-for での配列 ref,関数 ref など,すべてのユースケースをサポートしています.onScopeDispose を活用することで,適切なクリーンアップも保証されています.
