VueのReactiveのコードを少しだけ読む

Vueのアップデートをしたらpackages/reactiveの変更で不具合が生じていたので、この際少しだけ調べる。

ここに概要が載っているが、要約すると
- proxyオブジェクトを使う
- getする際にtrack (effectを登録)する
- setする際にtrigger(effectを実行)する
といったかなりシンプルな仕組み。
中でやってることはObservere/Subscriptionの仕組みと同様

deep-diveで説明されていたように、内部でproxyオブジェクトを生成して返却している。
deep-diveの下の例と比べると、第二引数のオブジェクトが、別メソッドに切り分けられているので、次はそこを読む。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
あとはweakMapでproxyオブジェクトを管理して、既にそのオブジェクトのproxyオブジェクトがあった場合はそちらを返すような処理がある。

proxyオブジェクト生成時の第二引数に渡ってるオブジェクトはMutableReactiveHandler のインスタンス。
MutableReactiveHandlerの実装は
ここで、proxyのオブジェクトのhandlerのメソッドを少しだけ参照しておく

脇道に逸れたが、
このhandlerオブジェクトがdeep-diveの概要のように、
getメソッドでtrack ( effectの登録)、setメソッドでtrigger(effectの実行)がなされているかを
見てみる。

get
ここでtrackしてそう。
set
この辺でtriggerしてそう。

track
trackの処理は
trackEffectは

trigger
triggerの処理は
triggerEffectは

dep
depはeffectを管理するためのmapっぽい

effect
effectって結局なんだっけ...
trackする際に登録してるactiveEffectはグローバス変数っぽい...
と思ったけど exportされてるやつはreadonlyってことで良いのかな。
このファイルの二箇所でしか書き変わってなさそう?
ReactiveEffectをrunした時にactiveEffectが更新されるらしい?

ここまでのコードリードでReactiveEffectのインスタンスが生成されていないが、
それは当たり前だった。
多分computedとかを使った時にeffectが作成される

computedの内部でeffectが生成されていた

ちょっと追えなくなってきたが、
deep-diveの例をEffectオブジェクトを使うように拡張してcomputedを作ってみよう

effect.dirty
このフラグがtrueじゃないとeffectが実行されない。
ちなみに、このEffect.dirtyの判定に修正が入ったことで担当プロジェクトでは不具合が発生している。
(お陰で久々にコードリードをする機会になった)

完全理解。
class Observable {
const subscriptions = []
on = (callback) => {
subscriptions.push(callback)
}
next = () => {
subscriptions.forEach((callback) => callback())
}
}
内部でSubscriptionの配列を持って購読をする際に配列に追加する。
というのが基本の処理。
Vueの場合はcomputed
やwatch
がsubscribeみたいなものと考えると良い
let activeEffect = null;
const effect = (fn) => {
const _effect = (fn) => {
activeEffect = _effect;
fn();
activeEffect = null;
}
_effect();
}
const computed = (fn) => {
let value = fn();
effect(() => value = fn();)
return {
value
}
}
computedを実行すると、内部でeffectが生成されて実行される。
effect実行前に、activeEffectとして自身を参照できるようにする。
effectを実行し、ref.valueのアクセスしたタイミングでtrackを開始。ref.valueが更新されたときに
effectが実行されるようになる。

const ref = (value) => {
return {
get value() {
track(ref)
return value;
},
set value(newVal) {
value = newVal;
trigger(ref);
},
};
};
let activeEffect = null;
const effect = (fn) => {
const _effect = () => {
activeEffect = _effect;
fn();
activeEffect = null;
}
_effect();
};
const targetMap = new WeakMap();
const track = (target) => {
let deps = targetMap.get(target);
if (deps === undefined) {
deps = new Set();
targetMap.set(target, deps);
}
if (activeEffect && !deps.has(activeEffect)) {
deps.add(activeEffect);
}
};
const trigger = (target) => {
const deps = targetMap.get(target);
if (deps === undefined) {
return;
}
deps.forEach((effect) => {
effect();
});
};
const computed = (getter) => {
let value;
effect(() => {
value = getter()
})
return {
get value() {
return value
}
}
}
const count = ref(0)
const doubled = computed(() => {
return count.value * 2
})
console.log(count.value, doubled.value)
count.value = 2
effect(() => {
console.log(count.value, 'changed')
})
console.log(count.value, doubled.value);

watchやwatchEffectはreactive packageの外にあった。
内部でReactiveEffectを作成している。