6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HajimariAdvent Calendar 2024

Day 6

vueのreactiveをチョットダケ自作してみた

Posted at

はじめに

vueを勉強し始めてホンノチョット分かるようになってきて、内部的にどんな感じになっているんだろうなと気になって「自分でチョットダケ作ってみたいな」と思いました。

もちろん、Vueのソースコードを直接見ることもできるのですが、まずは自分で書いてみることで、どういう仕組みで動いているのかを理解しようと思いmini-reactiveを作ってみました!

本当に雰囲気で書いているので改善ポイントはいっぱいあると思います...ご容赦ください。。。
僕のコードでは全然複雑な操作は出来ないので、、、本家の凄さを痛感しました...

リアクティブ

Vueのリアクティブシステムは、データの変更に自動的に反応してビューを更新する仕組みです。簡単に言うと、あるデータが変わったら、それに依存している他のデータも変わる、という機能です。

リアクティブオブジェクトは JavaScript プロキシ であり、通常のオブジェクトと同じように動作します。違いは、Vue がリアクティブオブジェクトのすべてのプロパティのアクセスや変更をインターセプトして、リアクティビティーの追跡やトリガーを行うことができることです。

自作してみた

どうやって作るか

僕が自作したリアクティブシステムのポイントは、あるデータが変更されたときに、そのデータに依存している処理(副作用)が自動で実行されることです。これを実現するために、Proxyを活用しました。

具体的には、次のように実装しました。

  • getはプロパティを読み取るときに呼ばれるので、依存関係を記録するために使う
  • setはプロパティを変更するときに呼ばれるので、その変更に関連する処理を再実行するために使う

依存関係の管理(track/trigger)

データの変更がどの処理に影響を与えるかを管理することです。つまり、どのデータに依存している処理を追跡し、そのデータが変わったときにその処理を実行する必要があります。

  • trackは、どのデータがどの処理に依存しているかを記録
  • triggerは、データが変更されたときに、依存する処理を再実行するために使う
let activeEffect = null;  // 現在アクティブな副作用(処理)

const track = (target, key) => {
  let dependencyMap = targetMap.get(target);
  if (!dependencyMap) {
    dependencyMap = new Map();
    targetMap.set(target, dependencyMap);
  }
  dependencyMap.set(key, activeEffect);  // 依存関係を記録
}

const trigger = (target, key) => {
  const dependencyMap = targetMap.get(target);
  if (!dependencyMap) return;

  const effect = dependencyMap.get(key);
  effect();  // 関連する副作用を再実行
}

effect

副作用(データが変更されたときに実行される処理)を登録するためのものです。この関数は、データがどの処理に依存しているのかを記録し、その後そのデータが変更されたときにその処理を再実行する仕組みです。
今回はconsole.logで出力するだけです。

実行の流れ

(1) reactive関数

reactive関数は、オブジェクトをProxyでラップします。Proxyは、オブジェクトの操作(プロパティへのアクセスや変更)をトラップする機能を提供します。これによって、プロパティへのアクセスや変更が行われる度に、getやsetのトラップが呼ばれます。

const handler = {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    track(target, key);  // データが読まれたことを追跡
    return res;
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver);
    trigger(target, key);  // データが変更されたことを通知
    return res;
  }
}

const reactive = (target) => {
  return new Proxy(target, handler);  // Proxyでラップして監視を開始
}

(2) effect関数

effect関数は、副作用(データが変更されたときに実行される処理)を登録するための関数です。この副作用は、データ(リアクティブなオブジェクト)を読み取った際に実行されます。

最初に副作用関数を登録し、その副作用が即時実行されます。実行中にデータが読み取られた場合、それに依存する副作用が追跡されます。これにより、後でデータが変更された際に、依存する副作用を再実行できます。

const effect = (fn) => {
  activeEffect = fn;  // 現在実行中の副作用を記録
  activeEffect();  // 即時実行して依存関係を追跡
  activeEffect = null;  // 実行後にactiveEffectをリセット
}

(3) trackとtrigger

  • track関数は、getトラップ内で呼ばれ、プロパティが読み取られたことを追跡します。これにより、特定のプロパティに依存している副作用を記録します。
  • trigger関数は、setトラップ内で呼ばれ、プロパティが変更されたことを通知します。それによって、変更されたプロパティに依存している副作用を再実行します。
const track = (target, key) => {
  let dependencyMap = targetMap.get(target);
  if (!dependencyMap) {
    dependencyMap = new Map();
    targetMap.set(target, dependencyMap);
  }
  dependencyMap.set(key, activeEffect);  // 依存関係を記録
}

const trigger = (target, key) => {
  const dependencyMap = targetMap.get(target);
  if (!dependencyMap) return;

  const effect = dependencyMap.get(key);
  effect();  // 関連する副作用を再実行
}

まとめ

mini-reactiveと呼ぶには恐れ多いのですが、、、チョットダケ自作してみることで、よりVueの中身について少しだけ知れましたし、何よりJavascriptの勉強になりました。

もっと本格的に書くなら、chibivueとかで学ぶのが良いんだろうなと思っています!
(vueの仕組みについてより深く知るなら是非!)

補足:ソースコード

<script type="module">
  import { effect, trigger, reactive } from './index.js';

  // reactiveでオブジェクトを作成
  const object1 = reactive({
    hoge: 0,
    fuga: 1,
  });

  const object2 = reactive({
    hoge: 0,
  });

  // effectでリアクティブな副作用を登録
  effect(() => {
    console.log("effect1...", object1.hoge)
  })

  effect(() => {
    console.log("effect2...", object2.hoge)
  })

  effect(() => {
    console.log("effect3...", object1.hoge)
  })

  // オブジェクトのプロパティを変更
  object1.hoge = 1;  // これによりeffect1とeffect3が再実行される
  object2.hoge = 2;  // これによりeffect2が再実行される
  object1.fuga = 3;  // effect3には影響しない(fugaはobject1のプロパティ)
</script>
// Proxyのハンドラーを定義
const handler = {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    // プロパティにアクセスされた時にtrack関数を呼び出し、依存関係を追跡
    console.log("get:", target, "key:" + key, "res:" + res);
    track(target, key);  // 依存関係を記録
    return res;
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver);
    // プロパティが変更された時にtrigger関数を呼び出し、再評価
    console.log("set:", target, "key:" + key, "value:" + value);
    trigger(target, key);  // 依存する副作用を再実行
    return res;
  }
}

// reactive関数でProxyオブジェクトを返す
const reactive = (target) => {
  return new Proxy(target, handler);  // Proxyでラップすることで、get/setを監視
}

let activeEffect = null;  // 現在アクティブな副作用を格納

// effect関数でリアクティブな副作用(callback)を登録
const effect = (fn) => {
  activeEffect = fn;  // 現在の副作用をactiveEffectに格納
  activeEffect();  // 副作用を即時実行(このタイミングでgetが呼ばれる)
  activeEffect = null;  // 副作用が実行された後にactiveEffectをnullにリセット
}

// オブジェクトとcallbackを関連付けるためのmap
const targetMap = new WeakMap();

const track = (target, key) => {
  console.log("track", target, "activeEffect", activeEffect);
  let dependencyMap = targetMap.get(target);
  
  if (!dependencyMap) {
    dependencyMap = new Map();
    targetMap.set(target, dependencyMap);  // 新たなターゲットを追加
  }

  // targetMapに対してキー(プロパティ名)とその依存するeffectを設定
  dependencyMap.set(key, activeEffect);
}

const trigger = (target, key) => {
  const dependencyMap = targetMap.get(target);
  if (!dependencyMap) return;

  const effect = dependencyMap.get(key);
  if (effect) {
    effect();  // 関連する副作用を再実行
  }
}

export { effect, trigger, reactive }
6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?