最近、Vueの原理についての記事をいくつか読んできました。これらの記事の助けを借りて、私も何度かVueのソースコードを理解しようと試みました。そして、自分自身でコンテンツを出力する時が来たと感じました。他の記事とは異なる視点から、皆さんにVueを紹介できればと思います。
このトピックはもちろん、複数のパートに分けてVueのソースコードを解説しますが、最初の記事では最もクラシックなVueのリアクティブ原理について話します!
原理を正式に説明する前に、以下のいくつかの概念をまずはっきりさせる必要があると思います ↓
Dep
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のソースコードに簡潔かつ理解しやすく示されています。
mountComponent
のvm._watcher = new Watcher(vm, updateComponent, noop);
initComputed
のwatchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
$watcher
のvar 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
関数は非常に重要であり、また長くもないため、ここに直接貼り付けて説明するのが便利です。
まず、リアクティブオブジェクトの各プロパティは「依存関係」ですので、最初に各値に対して Dep を作成します(Vue 3 ではクロージャは不要です)。
次に、3つの主要なパラメータを見てみましょう:
- obj: 現在のリアクティブ処理が必要な値が存在するオブジェクト
- key: 値のキー
- val: 現在の値
この値には、以前に独自の getter、setter が定義されている可能性もあるため、Vue のリアクティブ処理を行う前に、元の getter、setter を処理します。
getter
上記のコアフローで、getter 関数内で Dep と Watcher の関係を確立することが述べられています。具体的には、dep.depend()
に依存しています。
以下に、Dep と Watcher が互いに呼び出すいくつかのメソッドを示します:
これらの関数を通じて、Dep と Watcher の複雑な関係がわかりますが、実際には、多対多のリストに互いに追加しているだけです。
Depのsubsには、同じDepを購読しているすべてのWatcherが見つかります。また、Watcherのdepsには、そのWatcherが購読しているすべてのDepが見つかります。
しかし、ここにはもう1つの隠れた問題があります。それはDep.target
がどのように設定されるのかということです。一旦置いておき、後で解答をします。
setter
まず、setter関数を見てみましょう。重要なのはdep.notify()
です。
理解するのは簡単です。Depは、購読者リスト(subs)のすべての人に更新を通知します。購読者はすべてWatcherであり、subs[i].update()
はWatcher.prototype.update
を呼び出します。
それでは、Watcherのupdate
が何をしているか見てみましょう。
ここで、2つのポイントが注目されると思いますので、少し深堀りしてみましょう 😂
- ポイント1:非同期更新でない場合、queueWatcherに移動します。後で非同期更新について説明しますが、とにかくqueueWatcherが一連の操作の後にrunが実行されることを知っておけば十分です。
- ポイント2:Watcherのcb関数は、watch、computed、およびコンポーネントの更新関数を処理する可能性があります。特に重要なのは、コンポーネントの更新関数であり、ここでVueのページの更新が行われています。理解を容易にするために、更新がここでトリガされることを知っておけば十分です。更新方法については後で説明します。
- ポイント3:lazyの場合、以下の手順は実行されず、データの更新がマークされるだけで、次に値を取得すると新しい値が計算されます。
このコードの重要なポイントは、現在の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