LoginSignup
4
1

More than 3 years have passed since last update.

Vue3 の reactivity 部分のソースコードを読む

Posted at

はじめに

あとで何か書く。

事前知識

読む対象のソースコード

ここを読みます。
https://github.com/vuejs/vue-next/tree/master/packages/reactivity/src

Vue2 では、Object.defineProperty() を使用してリアクティブのロジックは実装されていましたが、Vue3 からは Proxy による実装に変更されています。Proxy による実装がどのようにされているかをソースコードをベースに確認します。

ソースコードは全部は読めないので、Composition API でリアクティブなコードを一番シンプルに書くと、以下のようになると思うので、このコードで実行される部分をターゲットにソースコードを読んでいきます。

import { reactive, effect } from 'vue';

// Object
const reactiveObj = reactive({ a: 1, b: 2 })

effect(() => {
  reactiveObj.b = reactiveObj.a * 10
})
console.log(reactiveObj)
// { a: 1, b: 10 }

reactiveObj.a = 10

console.log(reactiveObj)
// { a: 10, b: 100 }

大まかな流れ

// { a: 1, b: 2 } を Proxy オブジェクトに変換する。
const reactiveObj = reactive({ a: 1, b: 2 })

// reactiveObj.a の値が更新されたときに実行する関数として effect 関数を登録する。
effect(() => {
  reactiveObj.b = reactiveObj.a * 10
})
console.log(reactiveObj)
// { a: 1, b: 10 }

// reactiveObj.a の値が更新されたので、effect 関数を実行する
reactiveObj.a = 10

console.log(reactiveObj)
// { a: 10, b: 100 }

細かな流れ

Proxy オブジェクトへの変換

この部分、読みます。

const reactiveObj = reactive({ a: 1, b: 2 })

ソースコードの該当箇所は、以下です。
https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/reactive.ts

reactive 関数は、渡されたターゲットオブジェクト({ a: 1, b: 2 })を Proxy オブジェクトへ変換しています。ただ、実際の変換は reactive 関数ではなく、createReactiveObject 関数がしているので、reactive 関数は、渡されたターゲットオブジェクトと Proxy オブジェクトのハンドラー(mutableHandlersmutableCollectionHandlers)を createReactiveObject 関数に渡しているだけです。

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

createReactiveObject 関数は、ターゲットオブジェクトを Proxy オブジェクトに変換しています。createReactiveObject 関数の行数は、40 行くらいありますが、そのほとんどはメイン処理ではありません。メイン処理以外が何をやっているかと言うと、例えば、Proxy オブジェクトに変換する必要かあるかどうかをチェックしたり、どの Proxy ハンドラーを使うかを判定したり(ターゲットオブジェクトが ObjectMap かで使うハンドラーが異なるため)と...
メインの処理( Proxy オブジェクトに変換)は、return を見れば分かる通り、Proxy オブジェクトが返されています。

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {

  // ...省略

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

ここまでで Proxy オブジェクトへの変換が完了しました。

reactiveObj.a の値が更新されたときに実行する関数として effect 関数を登録する

この部分、読みます。

effect(() => {
  reactiveObj.b = reactiveObj.a * 10
})

ソースコードの該当箇所は、以下です。
https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/effect.ts

effect 関数では、リアクティブなオブジェクトが更新されたときに、発火させたい関数を実行します。effect 関数が実行されることで、reactiveObj.b = reactiveObj.a * 10 が実行されます。effect 関数の実行時では、reactiveObj は既に Proxy オブジェクトであるため、reactiveObj.a は、Proxy ハンドラーのゲッターによって呼ばれます。ゲッターの中で reactiveObj.a の値が更新されたときに実行する関数として effect 関数を登録する処理はしています。effect 関数自体は渡された関数を実行しているだけです。
createReactiveEffect 関数は、ソースコード読み切れていないので割愛します。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

Proxy のゲッターの呼び出し

この部分、読みます。

// effect(() => {
  reactiveObj.b = reactiveObj.a * 10
// })

この部分は、ゲッターとセッターの2パートに分かれています。 reactiveObj.a の値を取得しているゲッターと、reactiveObj.b の値を更新しているセッターです。セッターは、後続にある別のコードでソースコードを読むので、ここではゲッターに関するコードを読みます。

ソースコードの該当箇所は、以下です。
https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/baseHandlers.ts

createGetter 関数は Proxy ハンドラーのゲッターなので、ゲッターとしてのメイン処理である取得対象の値を返しています。ただ、これともう一つ大事なことを処理しており、それは依存関係の構築です。reactiveObj.a の値を使用して、reactiveObj.b = reactiveObj.a * 10 の計算を実行しているため、reactiveObj.b の値は reactiveObj.a の値に依存しています。そのため、 reactiveObj.a の値が更新されたら、 reactiveObj.b の値も更新する必要があります。まとめると、reactiveObj.a の値が更新された場合は、reactiveObj.b = reactiveObj.a * 10 を実行する必要があります。これが依存関係の構築で、それをしているのが track 関数です。(依存関係の構築までが track 関数なので、依存関係にある関数の実行はまた別の関数がしています。)
書き忘れていましたが、ゲッターの方は、 Reflect.get() によって実行されています。

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {

    // ...省略

    const res = Reflect.get(target, key, receiver)

    // ...省略

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // ...省略

    return res
  }
}

track 関数では、依存関係の構築をしています。
以下は、かなり思い切って省略していますが、メイン処理としては activeEffect (= effect 関数)に格納されている reactiveObj.b = reactiveObj.a * 10activeEffect.deps にプッシュしています。 ここでプッシュされた関数が reactiveObj.a の値が更新された時に実行されます。

export function track(target: object, type: TrackOpTypes, key: unknown) {

  // ...省略

  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)

    // ...省略

  }
}

Proxy のセッターの呼び出し

この部分、読みます。
reactiveObj.a の値を更新しているセッターです。

reactiveObj.a = 10

ソースコードの該当箇所は、以下です。
https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/baseHandlers.ts

reactiveObj.a が更新されたため、Proxy オブジェクトのセッターが呼ばれます。セッターの処理は大きく2つあり、1つは reactiveObj.a の値の更新であり、Reflect.set() によって実行されます。もう1つは、依存関係として登録した関数の実行です。

(依存関係の構築までが track 関数なので、依存関係にある関数の実行はまた別の関数がしています。)

上記の別の関数trigger 関数です。

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {

    // ...省略

    const result = Reflect.set(target, key, value, receiver)

    // ...省略

    trigger(target, TriggerOpTypes.SET, key, value, oldValue)

    // ...省略

  }
}

trigger 関数は、track 関数によって登録された関数を実行します。targetMap.get(target) によって取得した依存関係を add 関数で effects という Set オブジェクトに追加しています。そして、最後に effects.forEach(run) で実行し、effect 関数が実行されます。

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {

  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {

    // ...省略

  } else if (key === 'length' && isArray(target)) {

    // ...省略

  } else {

    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // ...省略

}

  const run = (effect: ReactiveEffect) => {

    // ...省略

    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}

まとめ

けっこう素人なので読み間違いあるかもしれません。以上終わり。

4
1
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
4
1