【Vue.js】refを実装して仕組みを理解する
どうもフロントエンドエンジニアのoreoです。
前回はVue.js Reactivity APIのreactiveに関して解説しました!この記事では、同じくReactivity APIのrefの仕組みについて、実装しながらその仕組みを知りたいと思います。
1 Composition APIのrefとは?
refでは、プリミティブな値をリアクティブ化することができます。下記のように定義されており、valueプロパティを持ったオブジェクト型を返します。
function ref<T>(value: T): Ref<UnwrapRef<T>>
interface Ref<T> {
value: T
}
参考
余談になりますが、reactiveとrefはそれぞれメリットデメリットがあります。個人的には、reactiveは分割代入時にリアクティブ性が無くなるタイミングが分かりずらい為、refをよく使っています。
※reactiveとrefの比較については、👇の記事でよく纏まっていました。
2 refを実装してみる
それでは、refを実装します。まずは、👇のようなJavaScriptのコードを考えます。
let price = 100;
let salePrice = price * 0.9;
console.log(salePrice); //90が出力
//priceを更新
price = 200;
console.log(salePrice); //90が出力。priceを更新してもsalePriceは再計算されない
このままではpriceを更新しても、salePriceの値は更新されません。priceをリアクティブ化することで、priceを変更するとsalePriceも更新されるようにしたいと思います。
2-1 reactiveをラップする方法
単純に、reactiveをラップすることで実装することができます。
function ref(intialValue) {
return reactive({ value: initialValue })
}
しかし、Vue.js作者のEvan You氏いわく、パフォーマンスの観点等からラップする方法は採用されなかったとのことです。
2-2 採用された実装方法
まず、reactiveを作成する時と同じように、targetMap、track、triggerなどを作成します。
const targetMap = new WeakMap();
let activeEffect = null;
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => {
effect();
});
}
}
function effect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}
各用語の解説とその役割は👇になります。実装内容の詳細は、前回の記事をご確認ください。
| 用語 | 役割 |
|---|---|
| activeEffect | 値の変更を検知して実行する処理を格納する変数 |
| effect | 引数に渡されたコールバック関数をactiveEffectに代入し、実行とresetを行う関数 |
| dep | activeEffectを格納するSetオブジェクト |
| depsMap | プロパティとdepを紐づけるMapオブジェクト |
| targetMap | オブジェクトとdepsMap紐づけるWeakMapオブジェクト |
| track | depにactiveEffectを格納する関数 |
| trigger | depに格納されているactiveEffectを実行する関数 |
次にrefを定義します。refでプリミティブな値をオブジェクトにラップして返しています。ポイントは、ラップしたオブジェクトのgetterでtrackを実行し、setterでtriggerを実行することです。
//refの定義
function ref(raw) {
//返すオブジェクトを定義
const r = {
//getterが呼ばれるときにtrackする
get value() {
track(this, "value");
return raw;
},
//setterが呼ばれるときにtriggerする
set value(newVal) {
raw = newVal;
trigger(this, "value");
},
};
return r;
}
getter・setterにそれぞれtrackとtriggerを差し込むことで、👇のようにpriceを変更するとsalePriceも更新されるようになりました。これでrefの完成です!
const price = ref(100);
let salePrice = 0;
//salePriceは、refを使っているのでvalueでアクセスする
//price.valueでアクセスした際に、trackが実行される
effect(() => {
salePrice = price.value * 0.9;
});
console.log(salePrice); //90が出力
//priceを更新(triggerの実行)
price.value = 200;
console.log(salePrice); //180が出力
ここでのtargetMapのイメージは👇になります。

また、targetMapの確認すると👇のようになっています。

3 最後に
reactiveの内部実装がわかっていれば、比較的簡単にrefは実装できました!Vue3のソースコードで、今回実装したような処理が記載されているのは下記になりますので、興味のある方は覗いてみてください!
packages/reactivity/src/baseHandlers.tspackages/reactivity/src/reactive.tspackages/reactivity/src/ref.tspackages/reactivity/src/effect.ts
4 参考
Discussion