Vueっぽいものを実装してComputedの仕組みを探る
作成日: 2022-10-28
VueのComputedのやや魔法っぽい、依存関係の自動導出についての仕組みを、簡単な実装で解説します。実装から効率良い更新方法が自動で求まる良い仕組みです。
フロントエンドライブラリのVue
JSのフロントエンドライブラリの一つです。Reactに押されてやや落ち目です。
JSが操作するModelを始めに定義し、ModelをTemplateが解釈して仮想DOMを構築するライブラリです。
Model → Template
VueのComputedとは
Model → Computed → Template
ModelをTemplateをから扱いやすくするために、Computedは間に挟まる構造です。Computeの値はModelから自動的にComputedが計算されます。計算された値をTemplateに使用できます。
computed: {
name() {
return `${this.firtName} ${this.lastName}`;
}
}
上の例では、Computedの
name
は、Modelの firstName
とlastName
から自動で計算されます。Computedの部分的な再計算
Modelが変更されると、Computedも変更されたModelの値を使って再計算されます。
Vueは再計算のとき、全てのComputedが再計算されるわけではありません。変更されたModelの値に影響のあるComputedだけが再計算されます。部分的な再計算によって仮想DOMも更新されるます。全体としての効率が良くなります。
ところで、この部分的な再計算はどのように実装されているのでしょう。Computedの実装では単にModelの値を直接使っているだけです。どのようにして、再計算が必要なComputedを見つけるのでしょうか。
Computedを実装してみる
Computedを実装したのがこちらの例です。ボタンを押すとテキストが変化します。
const data = {
firstName: "Super",
lastName: "Alice",
age: 10,
}
const computed = {
name(self) {
// Modelを使ってComputedを定義
return `${self.data.firstName} ${self.data.lastName}`;
},
hello(self) {
return `Hello, ${self.computed.name} (${self.data.age})`
},
};
ボタンを押すと、メソッドが呼ばれて、dataが更新されます。名前を書き換えるLouderではlastName→name→helloと更新されます。歳を増やすGrowでは、age→helloと更新されます。無関係な
Computedは再計算されていないことに注目しましょう。
const method = {
louder(self) {
// Modelを変更する
self.data.lastName += "!";
},
grow(self) {
self.data.age++;
},
}
Computeクラスがdataとcomputedとmethodを処理することで、Computedを実現しています。
window.compute = new Compute(data, computed, method);
Computeクラスでは次の2つのトリックが使われています。
- 依存グラフの自動生成
- Proxy
依存グラフの自動生成
先程の例で、「lastName→name→hello」や、「age→hello」といった更新順を表すグラフがありました。値の依存関係によって更新順を決めたグラフです。この依存グラフを自動生成できれば、Computedは実現できそうです。
依存グラフの生成は、helloが呼ばれたときに行われています。値の中で他の値が使われるときに、使われる様子を追跡して、依存グラフを構築します
this.deps
が依存を表します。 self.currentDeps
が呼び出し元のリストです。class DataValue {
get(self) {
if (self.currentDeps.length > 0) {
// 最後の呼び出し元を依存として追加する。
this.deps.add(self.currentDeps.slice(-1)[0]);
}
return this.value;
}
class ComputedValue {
get(self) {
if (self.currentDeps.length > 0) {
this.deps.add(self.currentDeps.slice(-1)[0]);
}
self.currentDeps.push(this);
if (this.cache === undefined) {
this.cache = this.computed(self);
}
self.currentDeps.pop()
return this.cache;
}
}
値が更新されるたびに、作られた依存グラフに従ってComputedを再計算します。
class DataValue {
set(self, value){
this.value = value;
// モデルが更新されたときに、
this.update(self);
}
update(self) {
// 依存元を再計算する。
this.deps.forEach(dep=>dep.update(self));
}
}
class ComputedValue {
update(self) {
this.cache = this.computed(self);
this.deps.forEach(dep=>dep.update(self));
}
}
では、単なるObjectの、どこに依存グラフを作るトリックが仕込まれているのでしょうか。
Objectのように振る舞うProxy
JavaScriptにはProxyという機能があります。Objectとして振る舞う値が、プロパティーの読み書きをフックして良からぬことをするためのAPIです。
Proxyによって、self.dataやself.computedはただのObjectではなくなっています。プロパティーが使われるたびに、プロパティーが誰によって使われたかを記録することで、依存グラフを構築します。のちに、this.dataのプロパティーが書き換えられると、ProxyがComputedを最計算します。
this.row_data = Object.entries(data).reduce(
(o, [k, v]) => {return {[k]: new DataValue(v), ...o}}, {});
this.data = new Proxy(this.row_data, {
get(t, prop) {
return t[prop].get(self);
},
set(t, prop, value) {
t[prop].set(self, value);
return true;
}
});
Proxyを使うことで、Modelを普通のObjectのように振る舞えるようになりました。素直にComputedを実装するだけで、依存グラフが得られる面白い仕掛けだと思います。