Vue3 の reactive vs ref

3 min読了の目安(約3500字TECH技術記事

概要

最近 this vs that を読んで、フロントエンドの理解を深めています. TypeScript の「interface vs type」や CSS の「display: none vs [hidden]」は、どちらが良いのか初学者には難しいですよね💦.
ただとても勉強になる一方、React の項目はあるのに Vue.js の項目がないです.

そこで、この記事では Vue.js の this vs that として reactive と ref について違いをまとめていこうと思います.

Vue3 のリアクティブについて

Vue3 のリアクティブは、Proxy を使って実現しています.Proxy を使うことで、ある変数が変更されるとそれに依存する変数も再計算されます.

// 使い方のイメージ
const data = new Proxy({
  value: 1
}, {
    set: function(obj, prop, newValue) {
        obj[prop] = newValue;
        func();
    }
});

let value = 2 * data.value
const func = () => {
    value = 2 * data.value
}

console.log(value); // 2

data.value = 4;

console.log(value); // 8

ここで大事なのは、 Proxy に渡すリアクティブにしたい変数を オブジェクト にしている点です.

FYI: Vue2 では、Proxy ではなく Object.defineProperty を使っていました. Object.definePropertyProxy は違う記事で書こうと思います.

reactive vs ref

結論、 オブジェクトをリアクティブにする場合は reactive、プリミティブ型をリアクティブにする場合は、ref を使えば良いです.

以下、コードベースで実装を追いながら説明します. まず、最初に reacrive のコードを見ていきましょう.

export function reactive(target: object) {
  // 省略 
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

  // 省略

  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

分かりやすくするために、コードを省略していますが大雑把に以下の流れで処理します.

  1. createReactiveObject が呼ばれる
  2. 引数の target がオブジェクトかどうか判定する
  3. オブジェクトの場合は Proxy の引数に渡される
  4. Proxy オブジェクトが返される

次に ref のコードを見てみましょう.

export function ref(value?: unknown) {
  return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val
  1. createRef を呼ぶ
  2. 渡ってきた値を元に RefImpl オブジェクトを作る
  3. RefImpl では _value から valueget, set を定義している
  4. RefImpl オブジェクトが返される

本当は tracktrigger のコードを追って、どのようにリアクティブ処理を最適化しているか見るべきですが記事が長くなるので省略します.

整理すると、次のようになります.

reactive ref
値がオブジェクトなら Proxy 型にして返す 値を元に RefImpl 型にして返す. RefImpl では value プロパティがあり、リアクティブになる

このような特徴のため、最初に書いたオブジェクトなら reactive、プリミティブ型なら ref が良いです.
ref には一つ注意点があり <script> タグ内で使う場合は、value プロパティを忘れないようにしてください.

const data = ref(0);

console.log(data.value); // 0

参考