【Vue.js】refを実装して仕組みを理解する
どうもフロントエンドエンジニアのoreoです。
前回はVue.js Reactivity APIのreactive
に関して解説しました!この記事では、同じくReactivity APIのref
の仕組みについて、実装しながらその仕組みを知りたいと思います。
ref
とは?
1 Composition APIのref
では、プリミティブな値をリアクティブ化することができます。下記のように定義されており、value
プロパティを持ったオブジェクト型を返します。
function ref<T>(value: T): Ref<UnwrapRef<T>>
interface Ref<T> {
value: T
}
参考
余談になりますが、reactive
とref
はそれぞれメリットデメリットがあります。個人的には、reactive
は分割代入時にリアクティブ性が無くなるタイミングが分かりずらい為、ref
をよく使っています。
※reactive
とref
の比較については、👇の記事でよく纏まっていました。
ref
を実装してみる
2 それでは、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
も更新されるようにしたいと思います。
reactive
をラップする方法
2-1 単純に、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.ts
packages/reactivity/src/reactive.ts
packages/reactivity/src/ref.ts
packages/reactivity/src/effect.ts
4 参考
Discussion