skip to content
usubeni fantasy logo Usubeni Fantasy

Vueのリアクティブ原理の解析

/ 16 min read

This Post is Available In: CN EN ES JA

最近、Vueの原理についての記事をいくつか読んできました。これらの記事の助けを借りて、私も何度かVueのソースコードを理解しようと試みました。そして、自分自身でコンテンツを出力する時が来たと感じました。他の記事とは異なる視点から、皆さんにVueを紹介できればと思います。

このトピックはもちろん、複数のパートに分けてVueのソースコードを解説しますが、最初の記事では最もクラシックなVueのリアクティブ原理について話します!

原理を正式に説明する前に、以下のいくつかの概念をまずはっきりさせる必要があると思います ↓

Dep

var Dep = function Dep() {
this.id = uid++;
this.subs = [];
};

Depの意味は、もちろんdependency(依存性、コンピュータの用語)です。

Node.jsプログラムを書く際には、よくnpmレポジトリの依存関係を使用します。Vueでは、依存性は具体的にはリアクティブ処理が施されたデータを指します。後で説明しますが、リアクティブ処理のキーファンクションの一つは、多くのVueの原理の記事で言及されるdefineReactiveです。

Depは各リアクティブデータとバインドされると、そのリアクティブデータは依存性(名詞)となります。後で説明するWatcherでは、リアクティブデータはウォッチャー、コンピューテッド、テンプレートの3つの関数に依存する可能性がある(動詞)と述べます。

subs

Depオブジェクトにはsubsというプロパティがあります。これはsubscriber(購読者)リストの配列を意味します。購読者はウォッチャー関数、コンピューテッド関数、ビューアップデート関数のいずれかです。

Watcher

WatcherはDepで言及されている購読者です(後のObserverオブザーバーとは混同しないでください)。

Watcherの機能は、Depの更新に迅速に応答することです。これは一部のアプリのサブスクリプションプッシュのようなもので、あなた(Watcher)は特定の情報(Dep)を購読し、情報が更新されるとお知らせします。

deps

Depがsubsプロパティを持っているのと同様に、Watcherオブジェクトもdepsプロパティを持っています。これにより、WatcherとDepは多対多の関係を形成し、関連するオブジェクトが削除された場合には適時更新されるようになっています。

Watcherの生成方法

上記で何度も言及されたウォッチャー、コンピューテッド、テンプレートの生成方法は、Vueのソースコードに簡潔かつ理解しやすく示されています。

  • mountComponentvm._watcher = new Watcher(vm, updateComponent, noop);
  • initComputedwatchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
  • $watchervar watcher = new Watcher(vm, expOrFn, cb, options);

Observer

Observerはオブザーバーであり、彼はリアクティブオブジェクト(または配列)を再帰的に観察(または処理)する責任を持ちます。プリントアウトされたインスタンスで注意すべきは、リアクティブなオブジェクトはすべて__ob__を持っているということです。これは既に観察されていることを証明しています。Observerは上記のDepとWatcherほど重要ではないので、少し理解すれば十分です。

walk

Observer.prototype.walkはObserverの初期化時に再帰的に処理される中核メソッドですが、このメソッドはオブジェクトの処理に使用されます。また、Observer.prototype.observeArrayも配列の処理に使用されます。

コアプロセス

上記のいくつかの概念の関係に従って、データのリアクティブな更新をどのように実現しますか?

まず、目標を定めましょう:データが更新されたときに自動的にビューをリフレッシュし、最新のデータを表示することです。

これが上記で言及したDepとWatcherの関係です。データはDepであり、Watcherがトリガーするのはページのレンダリング関数です(これが最も重要なウォッチャーです)。

しかし、新たな問題が発生します。DepはどのWatcherが自分に依存しているかをどのように知るのでしょうか?

Vueは非常に興味深い方法を採用しています:

  • Watcher のコールバック関数を実行する前に、現在の Watcher を記録しておきます(Dep.target を通じて)
  • コールバック関数でリアクティブデータを使用する場合、必ずリアクティブデータのゲッター関数が呼び出されます
  • リアクティブデータのゲッター関数内で、現在の Watcher を記録し、Dep と Watcher の関係を確立します
  • その後、リアクティブデータが更新されると、必ずリアクティブデータのセッター関数が呼び出されます
  • 以前に確立した関係に基づいて、セッター関数内で対応する Watcher のコールバック関数をトリガーできます

コード

上記のロジックは defineReactive 関数にあります。この関数はいくつかのエントリーポイントがありますが、ここでは比較的重要な observe 関数について説明します。

observe 関数では、Observer オブジェクトを作成し、Observer.prototype.walk を使用してオブジェクト内の値を個別にリアクティブに処理します。これには defineReactive 関数が使用されます。

defineReactive 関数は非常に重要であり、また長くもないため、ここに直接貼り付けて説明するのが便利です。

function defineReactive(obj, key, val, customSetter, shallow) {
var dep = new Dep()
depsArray.push({ dep, obj, key })
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 事前に定義されたゲッター/セッターに対応する
var getter = property && property.get
var setter = property && property.set
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
// The strange condition in the second half is used to determine the case where both the new and old values are NaN
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
// customSetter is used to remind you that the value you set may have problems
if ("development" !== "production" && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});

まず、リアクティブオブジェクトの各プロパティは「依存関係」ですので、最初に各値に対して Dep を作成します(Vue 3 ではクロージャは不要です)。

次に、3つの主要なパラメータを見てみましょう:

  • obj: 現在のリアクティブ処理が必要な値が存在するオブジェクト
  • key: 値のキー
  • val: 現在の値

この値には、以前に独自の getter、setter が定義されている可能性もあるため、Vue のリアクティブ処理を行う前に、元の getter、setter を処理します。

getter

上記のコアフローで、getter 関数内で Dep と Watcher の関係を確立することが述べられています。具体的には、dep.depend() に依存しています。

以下に、Dep と Watcher が互いに呼び出すいくつかのメソッドを示します:

Dep.prototype.depend = function depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};

これらの関数を通じて、Dep と Watcher の複雑な関係がわかりますが、実際には、多対多のリストに互いに追加しているだけです。

Depのsubsには、同じDepを購読しているすべてのWatcherが見つかります。また、Watcherのdepsには、そのWatcherが購読しているすべてのDepが見つかります。

しかし、ここにはもう1つの隠れた問題があります。それはDep.targetがどのように設定されるのかということです。一旦置いておき、後で解答をします。

setter

まず、setter関数を見てみましょう。重要なのはdep.notify()です。

Dep.prototype.notify = function notify() {
// 先に購読者リストを安定させる
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};

理解するのは簡単です。Depは、購読者リスト(subs)のすべての人に更新を通知します。購読者はすべてWatcherであり、subs[i].update()Watcher.prototype.updateを呼び出します。

それでは、Watcherのupdateが何をしているか見てみましょう。

Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};

ここで、2つのポイントが注目されると思いますので、少し深堀りしてみましょう 😂

  • ポイント1:非同期更新でない場合、queueWatcherに移動します。後で非同期更新について説明しますが、とにかくqueueWatcherが一連の操作の後にrunが実行されることを知っておけば十分です。
  • ポイント2:Watcherのcb関数は、watch、computed、およびコンポーネントの更新関数を処理する可能性があります。特に重要なのは、コンポーネントの更新関数であり、ここでVueのページの更新が行われています。理解を容易にするために、更新がここでトリガされることを知っておけば十分です。更新方法については後で説明します。
  • ポイント3:lazyの場合、以下の手順は実行されず、データの更新がマークされるだけで、次に値を取得すると新しい値が計算されます。
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// 新しい値を設定する
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, 'callback for watcher "' + this.expression + '"');
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};

このコードの重要なポイントは、現在のgetメソッドでDep.targetを設定していることです。(具体的なパスはrun -> get -> pushTargetです)

Dep.targetが存在する場合にのみ、コールバック関数cb(たとえば、ページのレンダリング関数は典型的なウォッチャーのcbです)が呼び出されたときに、Dep.prototype.dependが実際に機能するようになります。その後のロジックでは、リアクティブデータの取得に戻り、すべてがつながります!これが上記のdepend()の未解決の問題の答えです。

要約

  • Depはデータと関連付けられ、データが依存関係になれることを示します。
  • ウォッチャーには、ウォッチ、コンピューテッド、レンダリング関数の3種類があり、これらの関数は依存関係のサブスクライバになります。
  • ObserverはDepの処理のエントリーポイントであり、リアクティブデータを再帰的に処理します。
  • ウォッチャーのコールバック関数は、リアクティブデータを使用する際に最初にDep.targetを設定します。
  • ゲッター関数内のリアクティブデータは、Dep.targetを介して呼び出し元を知り、サブスクライバと依存関係を確立します。
  • セッター関数内のリアクティブデータは、サブスに通知してデータが更新されたことをすべてのサブスクライバに通知します。
  • サブスクライバがビューの更新関数(updateComponent -> _update)である場合、ユーザーはリアクティブデータが更新されるたびにページの更新を見ることができ、リアクティブな更新効果が実現されます。

このアルゴリズムは大まかに言って理解するのは難しくありませんが、実際にはこのアルゴリズムと協力して動作する他の多くのメカニズムがあり、完全なVueを構成しています。たとえば、上記で穴を掘ったように、更新キューとコンポーネントの更新関数の実装も学ぶ価値があります。

また、コードにはさらに多くの細かい点がありますので、興味のある方は自分で研究してみてください。

PS. 私の表現力があまりにも悪いため、知識の呪いもあり、この記事が本当にVueのリアクティブ原理を明確に説明できるかどうかはわかりません。わからない点があれば、コメントでお知らせいただければ幸いです。ありがとうございます 💡

唯一の参考リンク:Vue.js

评论组件加载中……