数年間、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 を紹介する際に、リアクティブデータは watch、computed、レンダリングテンプレートの 3 種類の関数に依存(動詞)する可能性があります。
subs#
Dep オブジェクトには subs 属性があり、これは配列です。これは簡単に推測できるように、subscriber(購読者)リストを意味します。購読者は watch 関数、computed 関数、ビュー更新関数である可能性があります。
Watcher#
Watcher は Dep に言及されている購読者です(後の Observer とは混同しないでください)。
Watcher の機能は、Dep の更新に即座に応答することです。いわば、いくつかのアプリの購読通知のように、あなた(Watcher)は特定の情報(Dep)を購読し、その情報が更新されると、読むように通知されます。
deps#
Dep が subs 属性を持つのと同様に、Watcher オブジェクトにも deps 属性があります。これにより、Watcher と Dep の間には多対多の関係が構成され、一方が削除されたときに関連オブジェクトを即座に更新できるようになります。
Watcher の生成方法#
上記で何度も言及した watch、computed、レンダリングテンプレートが 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 がトリガーするのはページレンダリング関数です(これは最も重要な watcher です)。
しかし、新たな問題が生じます。Dep はどの Watcher が依存しているかをどうやって知るのでしょうか?
Vue は非常に興味深い方法を採用しています:
- Watcher のコールバック関数を実行する前に、現在の Watcher が何であるかを記録します(Dep.target を通じて)
- コールバック関数内でリアクティブデータを使用すると、必然的にリアクティブデータの getter 関数が呼び出されます
- リアクティブデータの getter 関数内で現在の Watcher を記録し、Dep と Watcher の関係を確立します
- その後、リアクティブデータが更新されると、必然的にリアクティブデータの setter 関数が呼び出されます
- 以前に確立された関係に基づいて、setter 関数内で対応する 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
}
// 事前に定義された getter/setter に対応
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
// 後半部分の奇妙な条件は新旧値がどちらも NaN である場合を判断するためのものです
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// customSetter は設定した値に問題があるかもしれないことを知らせるために使用されます
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 値の 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 を見つけることができます。
しかし、ここには隠れた問題があります。それは 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(例えばページレンダリング関数は典型的な Watcher cb)が呼び出されると、Dep.prototype.depend
が実際に機能します。その後のロジックは、リアクティブデータの取得に戻り、すべてがつながります!閉じたループを形成します(滑稽)!これが上記の depend()
の未解決問題の答えです。
まとめ#
- Dep はデータと関連し、データが依存関係となることを示します
- Watcher には watch、computed、レンダリング関数の 3 種類があり、これらの関数は依存の購読者となることができます
- Observer は Dep を処理するためのエントリで、リアクティブデータを再帰的に処理します
- Watcher のコールバック関数はリアクティブデータを使用する際に、最初に
Dep.target
を設定します - リアクティブデータは getter 関数内で
Dep.target
を通じて呼び出し元を知り、呼び出し元との購読者と依存の関係を確立します - リアクティブデータは setter 関数内で subs を巡回し、すべての購読者にデータの更新を通知します
- 購読者がビュー更新関数(
updateComponent
->_update
)である場合、ユーザーはリアクティブデータが更新されるとページが更新されるのを見ることができ、リアクティブ更新効果を実現します
このアルゴリズムは粗略に言えば理解しやすいですが、実際には他の多くのメカニズムがこのアルゴリズムと協力して、完全な Vue を構成しています。例えば、上記で掘り下げた更新キューやコンポーネント更新関数自体の実装など、学ぶ価値があります。
また、コード内にはさらに多くの小さな詳細があり、興味のある方は自分で研究してください。
PS. 私の表現能力はあまり良くなく、知識の呪いもあって、この文章が本当に Vue のリアクティブ原理を明確に説明できるかどうか不安です。もし理解できない部分があれば、コメント欄でお知らせください。皆さん、ありがとうございます 💡