はじめに
あとで何か書く。
事前知識
-
Proxy
知っておかないと Vue3 のソースコードが読めないと思います。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy -
Reflect
何なのかは知っておかないと Vue3 のソースコードを読むときに詰まります。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Reflect -
WeakMap
何なのかは知っておかないと Vue3 のソースコードを読むときに詰まります。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/WeakMap -
Object.defineProperty()
Vue2 のリアクティブの実装に使用されていました。Vue3 からはリアクティブの内部実装がゴリッと変わっているため、見てみると楽しめる気がします。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty -
リアクティブの探求
いきなりソースコードは読めないと思うので、公式に記載されているリアクティブ実装の簡単な例を読むのが良いと思います。私は Vue Mastery ユーザーなのでこの記事はサラッとしか目を通していないです。
https://v3.ja.vuejs.org/guide/reactivity.html#vue-%E3%81%8B%E3%82%99%E3%81%93%E3%82%8C%E3%82%89%E3%81%AE%E5%A4%89%E6%9B%B4%E3%82%92%E8%BF%BD%E8%B7%A1%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95
読む対象のソースコード
ここを読みます。
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 オブジェクトのハンドラー(mutableHandlers
と mutableCollectionHandlers
)を 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
ハンドラーを使うかを判定したり(ターゲットオブジェクトが Object
か Map
かで使うハンドラーが異なるため)と...
メインの処理( 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 * 10
を activeEffect.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)
}
まとめ
けっこう素人なので読み間違いあるかもしれません。以上終わり。