🚴

【Vue.js】refを実装して仕組みを理解する

2022/06/27に公開

どうもフロントエンドエンジニアのoreoです。

前回はVue.js Reactivity APIのreactiveに関して解説しました!この記事では、同じくReactivity APIのrefの仕組みについて、実装しながらその仕組みを知りたいと思います。

https://zenn.dev/oreo2990/articles/e6dcfeb4721e54

1 Composition APIのrefとは?

refでは、プリミティブな値をリアクティブ化することができます。下記のように定義されており、valueプロパティを持ったオブジェクト型を返します。

function ref<T>(value: T): Ref<UnwrapRef<T>>

interface Ref<T> {
  value: T
}

参考
https://vuejs.org/api/reactivity-core.html

余談になりますが、reactiverefはそれぞれメリットデメリットがあります。個人的には、reactiveは分割代入時にリアクティブ性が無くなるタイミングが分かりずらい為、refをよく使っています。

reactiverefの比較については、👇の記事でよく纏まっていました。
https://zenn.dev/azukiazusa/articles/ref-vs-article

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を作成する時と同じように、targetMaptracktriggerなどを作成します。

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にそれぞれtracktriggerを差し込むことで、👇のように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

https://github.com/vuejs/core

4 参考

https://www.vuemastery.com/

Discussion