🏊

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

2022/07/25に公開

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

これまでVue.js Reactivity APIのreactiverefに関して解説しました!この記事では、computedの仕組みについて、実装しながら理解したいと思います。

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

https://zenn.dev/oreo2990/articles/7280f753118227

1 Composition APIのcomputedとは?

computedは、リアクティブな値を含むコールバック関数を引数にとり、valueプロパティを持ったオブジェクト型を返します。コールバック関数内のリアクティブな値が変更されると返り値も変更されます。computedは、下記の様に定義されています。

function computed<T>(
  getter: () => T,
  debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>

※今回の記事では、read-onlycomputedを扱い、writableは扱いません。

参考

https://vuejs.org/api/reactivity-core.html#computed

2 computedを実装してみる

まず、refを作成する時と同じように、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((eff) => {
      eff();
    });
  }
}

function ref(raw) {
  const r = {
    get value() {
      track(r, "value");
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      trigger(r, "value");
    },
  };
  return r;
}

function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}

各用語の解説とその役割は👇になります。実装内容の詳細は、reactiveの記事をご確認ください。

用語 役割
activeEffect 値の変更を検知して実行する処理を格納する変数
effect activeEffectの実行やresetを行う関数
dep activeEffectを格納するSetオブジェクト
depsMap プロパティとdepを紐づけるMapオブジェクト
targetMap オブジェクトとdepsMap紐づけるWeakMapオブジェクト
track depにactiveEffectを格納する関数
trigger depに格納されているactiveEffectを実行する関数

次にcomputedを定義します。effect関数の中で、コールバック関数の返り値をrefオブジェクトであるresultに代入することで、targetMapにコールバック関数を登録し、それを返します。

function computed(getter) {
  //refオブジェクトの作成
  let result = ref();

  //コールバック関数(getter)の返り値をresult.valueに代入
  effect(() => (result.value = getter()));

  return result;
}

let price = 100;
let discountLate = ref(0.9);

let salePrice = computed(() => {
  return price * discountLate.value;
});

console.log(salePrice.value); //90が出力

この時のtargetMapのイメージは👇になります。

この状態で、refオブジェクトであるdiscountLateを更新すると、triggerが実行され、salePriceも更新される様になります!これでcomputedの完成です!

discountLate.value = 0.7;
console.log(salePrice.value); //70が出力!!!

3 最後に

3回の記事に分けて、reactiverefcomputedを実装しました。内部実装を知っておくといろんな場面で応用が効きそうですね。Vue3のソースコードで、今回実装したような処理が記載されているのは下記になりますので、興味のある型は覗いてみてください!

  • packages/reactivity/src/baseHandlers.ts
  • packages/reactivity/src/reactive.ts
  • packages/reactivity/src/ref.ts
  • packages/reactivity/src/computed.ts
  • packages/reactivity/src/effect.ts

https://github.com/vuejs/core

4 参考

https://www.vuemastery.com/

Discussion