LoginSignup
0

posted at

Vue3: 「リアクティビティーの探求」を探求する

Vue 3/Composition APIの公式ドキュメントには、フレームワークのコア機能の一つであるリアクティビティについて、「リアクティビティーの探求」というページを作って公式の解説があります。フレームワークやライブラリが自身の構造を丁寧に説明してくれることって少ない感覚で、Vue歴が短い自分にとっては嬉しい & 興味深い解説ページでした。

リアクティビティーの探求 | Vue.js

いっぽうで、ページにあるコードは断片的で、そのままでは動かず再現が難しいです(実際難しかった)。

せっかくなら、と@vue/reactivityを見たり解説動画見たりしながら動くコードを作ってみました。この記事はその超初歩の部分を言語化したものです。

はじめに

リアクティビティの簡易な再現を試みることをゴールとします。そして、値の計算をリアクティブにできたとして、DOMレンダリングの問題が残りますがこれも扱いません。

なお、Vue Masteryから本格的な解説動画が出ているので、興味のある方は見てみると学びが深いです。実は、この記事は動画とVue 3のコードを読んで理解したことのサマリでもあります。

Reactivity in Vue 3 - How does it work?

コードの補足

記事等から再現を試みた際に、型というかオブジェクトの形がわからず苦労したので、できる限りTypeScriptで書きます。

記事に登場するコードは説明のために切り取ったり一部省略されているところがあります。前編はリポジトリに置かれているのでこちらもご参考ください。

記事に書ききれなかったコードの全容を見れます。

リアクティビティとはなんなのか

筆者は、はじめリアクティビティがいまいちわかったような気でわかっていませんでした。かんたんにどういうものかを整理します。

リアクティビティが実現するとは、以下のコードが成り立つ状態でもあります。

const price = 5
const quantity = 2
const total = price * quantity

console.log(total) // 10

const price = 8
console.log(total) // 16

通常、 total が評価されたあとで構成要素に変化があろうが結果が更新されることはないわけです。total がリアクティブになるなら、priceへの代入を検知してtotalが再計算されるようになります。

では、リアクティビティをどう達成するかなのですが、すごーくざっくりいうと

effectを構成するプロパティを変更追跡対象にして、変更を検知したらeffectを再評価する

といった感じになります。

もうちょっと実装の話をすると、再評価したいeffecttotal = price * quantityのような式)を集めるSetを用意して、要素の変更を検知したらSetから取り出して評価を実行する流れとなります。扱いやすいように色々ゴニョゴニョするけど、コアのアイデアは本当にそんな感じです。

ちなみに、公式の解説にもコードにも effect という命名が頻出します。日本語にするとニュアンスが抜け落ちてしまう感覚があるので、以後断りなく effect と書きますが「複数の要素からなる式」、くらいの認識で問題ありません。

function effect () {
  return price * quantity
}

見取り図はこのへんにして、具体的なコードで再現していきます。

effectを追跡する

Vue3のリアクティビティはMapやSetで変更対象の追跡登録・再評価を行うため、どこに何が入っているのか結構混乱します…

コードと図で1つずつ読み解きます。

まずは、先ほど上げたSetと各種関数を書きます。追跡には track 、再評価には trigger を定義します。 deptracktrigger の名前は本家コードにもそのまま登場するものです。

let price = 5
let quantity = 2
let total = 0

let dep = new Set()

let effect = () => { total = price * quantity }

const track = () => { dep.add(effect) }
const trigger = () => { dep.forEach(effect => effect()) }

track()
effect()

図で整理してみると…

スクリーンショット 2023-03-22 21.51.10.png

オブジェクトの導入

単純な数値や文字列だけにとどまらず、オブジェクトとそのプロパティも変更対象として検知したいことがあります。そこでプロパティをキーに dep を保持するMapを用意します。

const depsMap = new Map<any, Set>()

このマップはオブジェクトのプロパティをキーにdepを対応させるので、tracktriggerでそれぞれ対応付け・キーによる検索を実現させたいです。

const track = (key: unknown) => {
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(effect)
}

const trigger = () => {
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

追跡対象の登録、評価はそれぞれ呼び出すだけです。

track('quantity')
effect()

逆にわかりづらいかもですが…図にすると以下のような感じになります。

スクリーンショット 2023-03-23 8.29.31.png

depsMap はオブジェクトのプロパティと、プロパティに依存しているeffectの集合ということがわかりました。最後に、そもそも depsMap のキーを持つオブジェクト自体を格納しておくなにかがあると便利です(ないと不便)。オブジェクトをキーに、depsMap を持つ targetMap を定義します。

const targetMap = new WeakMap<any, Map<any, Set>>()

WeakMap はオブジェクトをキーにできるよわいMapです。ここでは、普通のMapと同じように使うので特殊性などの説明は割愛します。

WeakMap - JavaScript | MDN

track, triggerを targetMap から順番に dep を引くように直します。なお、get の過程でSetがなければnewして空箱を用意しておきます。

const track = (target: any, key: unknown) => {
  let dep = depsMap.get(key)
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(effect)
}

const trigger = (target: object, key: unknown) => {
  const dep = targetMap.get(target)?.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

tracktriggerを手動で呼び出して追跡と評価を実行してみます。

const price = 5
const quantity = 2
const product = { quantity * price }
track(product, 'quantity')
trigger()

なお、普段目にするVueの機能とは少し距離があるな、と感じると思います。実際のフレームワーク内部は、これに色々かぶせて refcomputed などを実現しています。

おわりに

ここで書いたようなアイデアがわかれば、Vueのコードが読みやすくなるしデバッグもいくばくか気楽になります。

記事で紹介した簡易再現コードのフルセットはリポジトリの core/packages/reactivity に実態があります。この記事や動画、公式ドキュメントと合わせてぜひご参照ください。

core/packages/reactivity at main · vuejs/core

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
What you can do with signing up
0