1.概要
前の章では、観測クラスとパブリッシャーは、propとdataによって定義されたプロパティによって確立されていますが、サブクラスは、現時点ではリリースクラスDepで空です。ウォッチャーを登録し、プロパティが変更されたときにそれを更新する方法。この章では引き続きWatcherのサブスクリプションを紹介します。
紹介する前に、まず、ユーザー定義の小道具とデータのどのメソッドまたは式でレスポンシブな変更を実装する必要があるかを考えてみましょう。
まず第一に、計算されたユーザー定義のウォッチが必要であり、ビューでも多数の属性式(前の例{{item.id}}など)を使用する必要があります。これらのデータはメソッドで使用されますが、すべて時間内に呼び出されるため、必要ありません。
実際、これらの3つのシナリオでは、ソースコードによってウォッチャーも作成されます。以前のブログを読んだことがある場合は、initWatcher、initComputer(パート5)、およびマウント(パート6)で簡単に紹介し、知識を得ました。 。
3つのウォッチャーは、ユーザーウォッチャー、コンピューティングウォッチャー、レンダーウォッチャーであり、この記事では、これまでの知識を組み合わせて、これらのウォッチャーの実現原理を詳しく紹介します。
二、ユーザーウォッチャー
実行例として次の時計を使用します。
data:{
msg:'this is msg',
items:[
{id:1},
{id:2},
{id:3}
]
}
watch: {
msg: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
....
}
次のように、initWatcherがウォッチャープロセスを作成する5番目の部分を確認します。
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
...
//创建watcher对象,进行监听
const watcher = new Watcher(vm, expOrFn, cb, options)
...
}
主な入力パラメーターは、vueオブジェクトであるvm、expOrFn、この例では属性msgオブジェクトである監視対象の式、cb、この例ではmsg属性の対応するメソッドであるコールバック関数です。ウォッチャーの定義は、src / core / observer / watcher.jsにあります。
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,//组件对象
expOrFn: string | Function,//待观察的表达式
cb: Function,//回调函数,更新的时候调用
options?: ?Object,
isRenderWatcher?: boolean
) {
//1、初始化变量
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
//2、解析表达式,获取getter方法
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
//3、依赖收集
if (this.computed) {
//对于计算属性watcher,此时没有立即进行依赖收集,在执行render函数时收集
this.value = undefined
this.dep = new Dep()
} else {
//对于普通watcher,调用get方法,进行依赖收集
this.value = this.get()
}
}
...
}
1.工法
最初にその構築方法を見てみましょう。主なプロセスは次のとおりです。
(1)変数を初期化します。主な変数は次のとおりです。
deepは、詳細に、つまりネストされた属性を持つ変数を監視するかどうかを示します。
計算され、計算された属性かどうかを示します
sync、更新時に同期を実行するかどうかを示します。
deps、newDeps、このウォッチャーに対応する維持されているパブリッシャー配列。
(2)。式を解析し、getterメソッドを取得します。expOrFnが関数タイプの場合は、直接getterメソッドに設定します。それ以外の場合は、parsePathメソッドを呼び出して属性オブジェクトを取得メソッドとして返します。この場合、後者が使用され、実行結果はvmですmsg]。
export function parsePath (path: string): any {
...
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
(3)依存コレクション、この部分は非常に巧妙に処理されます。非計算属性の場合、getメソッドが直接呼び出されます(計算属性のウォッチャーの場合は、表に表示されます)。getメソッドを引き続き見てください。
2. getメソッド
get () {
//1、将当前的watcher压栈
pushTarget(this)
let value
const vm = this.vm
try {
//2、核心代码,依赖收集
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
//3、后置处理
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
(1)独自のウォッチャーオブジェクトをスタックにプッシュし、グローバル変数Dep.targetを現在のウォッチャーオブジェクトとして設定します。
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
(2)getterメソッドを実行して、この属性のget hijackingをトリガーします。前の章で定義されたhijackingメソッドを確認します。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
//依赖收集
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
...
グローバル変数Dep.targetは、現在のウォッチャーオブジェクトを表し、空ではありません。引き続きDepクラスのDepメソッドを呼び出します
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
最後に、ウォッチャーのaddDepメソッドが呼び出されました。
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
//加入到watcher的dep数组
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
//加入到dep的sub数组
dep.addSub(this)
}
}
}
このメソッドはまずウォッチャーのnewDeps配列にdepを保存し、次にDepのaddSubを呼び出してウォッチャーオブジェクトをサブ配列に追加します。
addSub (sub: Watcher) {
this.subs.push(sub)
}
呼び出しプロセス全体は比較的円形であり、これの目的は、depとwatcherの間に二重にリンクされたリストを確立することです。
(3)後処理(ディープ処理を含む)、現在のウォッチャーオブジェクトのクリアなど。
プロセス全体が完了すると、ウォッチャーオブジェクトがmsg属性のDepオブジェクトのサブに追加されます。msgの依存関係モデルは次のとおりです。
三、コンピューティングウォッチャー
次の計算属性computeMsgは実行例です。
<div id="app">
{{computeMsg}}
...
</div>
var vm = new Vue({
data:{
msg:'this is msg',
items:[
{id:1},
{id:2},
{id:3}
]
}
watch: {
msg: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
computed:{
computeMsg:function(){
return "this is computed msg:"+this.msg
}
}
....
}
この例では、computeMsg計算属性が作成され、テンプレートで使用されます。msg属性は、computeMsg計算属性式で呼び出されます。
5番目の章でinitComputedメソッドを見てみましょう。
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
//1、循环计算属性
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
...
if (!isSSR) {
// create internal watcher for the computed property.
//2、为每个属性创建watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
...
//3、劫持数据变化,创建监听方法
defineComputed(vm, key, userDef)
}
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
....
}
//4、并对计算属性的getter和setter进行劫持
Object.defineProperty(target, key, sharedPropertyDefinition)
}
}
//getter方法劫持
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//依赖收集,将订阅类添加到
watcher.depend()
//返回值
return watcher.evaluate()
}
}
}
このメソッドでは、定義された計算属性がループされ、計算属性ごとにウォッチャーが作成され、データの変更を監視するようにゲッターメソッドが設定されます(計算属性のセッターメソッドはあまり使用されません)。ウォッチャーの主な入力パラメーター:vm、つまりvueオブジェクト、ゲッター、計算された属性の式、これはこの例のcomputeMsgに対応する式です。ウォッチャーの構築方法を見ていきます。
1.コンストラクタ
constructor (
vm: Component,//组件对象
expOrFn: string | Function,//待观察的表达式
cb: Function,//回调函数
options?: ?Object,
isRenderWatcher?: boolean
) {
...
// parse expression for getter
//1、对于计算属性,表达式即为getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
if (this.computed) {
//2、对于计算属性,创建了dep,此时没有立即进行依赖收集,
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
ウォッチャーの入力パラメーターvmはvueオブジェクトを表し、ゲッターは監視対象の式、つまり計算属性関数です。
ユーザーウォッチャーと比較すると、2つの異なる場所があります。
(1)expOrFnは属性を計算するための式であるため、型は関数であるため、分岐を取り、ゲッターを属性を計算するための式に設定します。
(2)。依存関係コレクションのgetメソッドは呼び出されませんが、depオブジェクトが作成され、オブジェクトは計算された属性に依存するウォッチャーを保存します。
計算された属性の依存関係は通常の属性とは異なります。これは、式に含まれている通常の属性(この例の属性msgなど)に依存し、他の呼び出し元(この例のcomputeMsgというテンプレート式など)にも依存しています。 ({{ComputeMsg}})。この例の依存関係モデルは次のとおりです。
次に、定義時にこれらの依存関係が収集されないため、いつ行われたのかという疑問が生じます。答えは、テンプレートが呼び出されたときです。
計算属性の本来の目的は、テンプレートの表現を簡略化し、ロジックが多すぎてテンプレートが複雑にならないようにすることです。したがって、依存関係はテンプレートが呼び出されたときにのみトリガーされ、定義のみが呼び出されないと依存関係が無駄になります。
テンプレートレンダーに関連するウォッチャー(レンダーウォッチャー)を紹介し、赤いマークの付いた依存関係のコレクションを完了する方法を見てみましょう。
四、レンダーウォッチャー
最初に、第6章で、マウント時のmountComponentメソッドについて紹介します。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
....
//2、定义updateComponent,vm._render将render表达式转化为vnode,vm._update将vnode渲染成实际的dom节点
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
//3、首次渲染,并监听数据变化,并实现dom的更新
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
....
}
当時、このウォッチャーには2つの機能があると述べました。1.最初のdomレンダリングと完全な依存関係コレクションを実現します。2.テンプレートに含まれる式の変更を監視して、更新を実装します。
ウォッチャーは、データ「レンダー」テンプレートの「ブリッジ」であり、レンダーウォッチャーと呼ばれ、そのコア部分は入力パラメーター式(2番目のパラメーター、updateComponent)に反映されます。ウォッチャーの初期化のメインプロセスを見てみましょう。
constructor (
vm: Component,//组件对象
expOrFn : string | Function,//待观察的表达式
cb: Function,//回调函数
options?: ?Object,
isRenderWatcher?: boolean
) {
....
if (typeof expOrFn === 'function') {//1、设置getter方法为表达式
this.getter = expOrFn
} else {
.....
}
if (this.computed) {
....
} else {
//2、执行get方法,进行依赖收集
this.value = this.get()
}
}
1. expOrFnは関数であるため、getterメソッドを式(updateComponentメソッド)に設定します。
2.これは、ユーザーウォッチャーのように計算された属性ではないため、getメソッドを実行すると、実際にはupdateComponentメソッドが実行されます。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
このメソッドには、vm._render()とvm._updateの2つの実行ステージが含まれています。これについては第6章で分析しているので、詳しくは説明しません。今日、私たちは以前に提起された質問への回答、依存関係のコレクションを実装する方法に焦点を当てています。
上記の計算された属性を例にとると、テンプレートでは「{{computeMsg}}」を使用しました。
<div id="app">
{{computeMsg}}
...
</div>
テンプレートがコンパイルされた後(前のコンパイルセクションを参照)、このセクションのレンダー式 "_s(computeMsg)"は、vm._renderの実行時に、computeMsgによって設定されたゲッターメソッドをトリガーします。
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//1、依赖收集,将订阅类添加到dep中
watcher.depend()
//2、返回值
return watcher.evaluate()
}
}
}
1. watcher.dependを呼び出して、現在のレンダーウォッチャーオブジェクトをcompteMsgのdepに追加し、依存関係のコレクションを完了します。
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
ここでのDep.targetはレンダーウォッチャーを指すことに注意してください。依存関係モデルは次のとおりです。
2. watcher.evaluate()を呼び出します。
evaluate () {
//如果有更新,则重新计算,否则返回缓存值
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
これは、計算ウォッチャーを指します。getメソッドは属性式です。
function(){
return "this is computed msg:"+this.msg
}
msg属性が式に含まれているため、実行プロセス中にmsg getモニタリングメソッドがトリガーされ、コンピューティングウォッチャーがmsg depに追加されて、依存関係のコレクションが完了します。最終的な依存関係モデルは次のとおりです。
これまでのところ、computeMsgの依存関係と依存関係が収集されています。また、テンプレートの共通属性を呼び出す依存関係収集プロセスを分析することもできます。
5.アップデートを配布する
現在、msg属性のdepパブリッシャーには2つの依存ウォッチャーオブジェクトが収集されています。msg値をリセットするとどうなりますか?
msg値がリセットされると、setメソッドがトリガーされます。
Object.defineProperty(obj, key, {
...
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
//核心部分,通知更新
childOb = !shallow && observe(newVal)
dep.notify()
}
})
パブリッシャーの通知メソッドを呼び出します。
//通知相关的watcher类更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
loop属性に関連付けられたウォッチャークラスは前述のとおりで、現時点では、depのサブコレクションにユーザーウォッチャーと計算されたウォッチャーの2つのオブジェクトがあります。それぞれの更新メソッドを呼び出します。
update () {
/* istanbul ignore else */
if (this.computed) {//计算属性watcher处理
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
//当没有订阅计算属性时,不需要计算,仅仅设置dirty为true,表示下次重新计算
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {//同步处理,立即执行
this.run()
} else {//异步处理,进入堆栈
queueWatcher(this)
}
}
更新は3つのブランチに分かれており、3つの状況を個別に分析します。
(1)属性処理を計算します。
(2)、同期処理、即時実行。
(3)、スタックに追加された非同期処理。
1.属性を計算する
計算属性のウォッチャーオブジェクトについて、計算属性がサブスクライブされているかどうかを確認します。サブスクライブされていない場合(つまり、計算属性のdepのサブセットが空であるかどうか)は、dirtyをtrueに設定し、次回再計算します。
この例のように、computeMsgはレンダーウォッチャーに依存しているため、elseブランチに入り、getAndInvokeメソッドを実行します。
this.getAndInvoke(() => {
this.dep.notify()
})
getAndInvoke (cb: Function) {
//获取最新的值
const 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) ||//object对象
this.deep//深度watcher
) {
// set new value
//更新值
const oldValue = this.value
this.value = value
this.dirty = false
//执行回调函数
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
このメソッドのコア部分は、着信コールバックcbメソッド、つまりthis.dep.notify()を実行することです。このとき、レンダーウォッチャーの更新が再帰的に呼び出され、更新メソッドが返されます。
2.同期処理
計算された属性(この例ではユーザーウォッチャー、レンダーウォッチャー)ではなく、同期処理が設定されている場合は、runメソッドが呼び出されます。など:
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
runメソッドはgetAndInvokeを呼び出します。メソッドのコア部分は次のとおりです。
(1)getメソッドを実行して値を取得します。レンダーウォッチャーの場合は、updateComponentメソッドを実行し、Vnodeを再生成し、パッチを適用してdomを更新します(次の章で詳しく説明します)。
(2)この例のmsgのウォッチ式などのコールバックcbメソッド(注:レンダーウォッチャーのcbは正午です)
3.非同期処理
非同期の場合、queueWatcherを呼び出してウォッチャーをスタックにプッシュします。このメソッドを使用する前に、キューのウォッチャーを更新する方法、つまり、src / core / observer / scheduler.jsのflushSchedulerQueueメソッドを見てみましょう
function flushSchedulerQueue () {
//1、设置标识位flush为true,标识正在刷新中
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
//2、将queue数组从小到大排序
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
//3、循环队栈queue,执行watcher.run方法,实现更新。
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
//4、重置相关的状态
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
コアステップを見てみましょう。
(1)キューが処理中であることを示すフラグビットフラッシュを設定します。
(2)キュー内のウォッチャーのIDに従って、小さいものから大きいもの(つまり、作成の順序)にソートします。
(3)quequeでウォッチャーをループし、runメソッドを実行して、更新を実装します。
(4)キューでの実行が完了すると、フラッシュ、待機など、関連する状態がリセットされ、次の実行を待ちます。
処理プロセス全体は比較的明確ですが、ステップ3では、キューが動的に変化する可能性があることに注意してください。
ここで振り返って、引き続きqueueWatcher、ウォッチャーをキューに追加する方法、およびflushSchedulerQueue実行をトリガーする方法を確認します。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {//对于同一个watcher,不会重复加入到queue,避免多次触发
has[id] = true
//1、将watcher加入队列中
if (!flushing) {//尚未刷新,则加入队栈,待执行
queue.push(watcher)
} else {//2、正在刷新中,则动态的插入到到对应位置。
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
//从后往前,查找对应位置
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
//3、通过nexttick执行queue中的watcher
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
ウォッチャーをキューチームスタックに追加します。2つのケースがあります。
(1)flushSchedulerQueueを実行しない場合は、ウォッチャーをキューに追加し、次の実行を待ちます。
(2)flushSchedulerQueueを実行すると、対応する位置が後ろから前に検索され、キューに挿入されます。
(3)nextTickを介してflushSchedulerQueueを呼び出し、キュー内のウォッチャーを更新します。VueのDOMは非同期に更新されます。nextTickは、DOMが更新された後に実行されることを保証します。これは、次のイベントループの「ティック」と見なすことができます。nextTickメカニズムは、より効率的な「一括」更新を実装します。
同じウォッチャーの場合、複数のトリガーを回避するためにキューに繰り返し追加することはできないことに注意してください。
6.まとめ
この章のロジックはより複雑です。誰もが理解できるさまざまな方法の呼び出し関係を要約します。