ホームページ >ウェブフロントエンド >Vue.js >Vue のウォッチャーとスケジューラーの詳細については、こちらをご覧ください。
この記事では、Vue のウォッチャーとスケジューラーを理解し、Vue のウォッチャーの実装原理を紹介します。
Vue は、データ検出メカニズムを通じて状態の変化を感知します。前の記事「Vue によるデータ検出の実装方法」では、Watcher オブジェクトについて説明しました。データが更新されると、更新があります。 this.title = '変更されたかどうかを監視' を実行すると、setter 関数は dep.notify を呼び出して、ウォッチャーに更新を実行するように通知します (具体的には watcher.update 関数を実行します)。
それでは、Vue がウォッチャーをいつ作成するのか、スケジューラーを通じてウォッチャー キューをスケジュールする方法、そしてウォッチャーの更新が最終的にビューのレンダリングにどのように反映されるのかについて説明します。この記事では主にこれら 3 つの問題に焦点を当てます。 Vue の Watcher の実装原理を紹介します。 [関連する推奨事項: "vue.js チュートリアル "]
コンポーネントをいつ作成するか (作成から破棄まで)一連のライフ サイクルを経た後、私たちがよく知っているのは beforeMount、mounted、beforeUpdate、updated であり、ライフ サイクルを理解すると、Watcher がいつ作成されたかを理解するのがはるかに簡単になります。 Vue は、マウント イベント、$watch 関数、computed および watch プロパティの 3 つの場所に Watcher オブジェクトを作成します。mount イベントは、通知をレンダリングするための Watcher を作成します。watch と computed によって作成された Watcher は両方とも、ユーザー定義のプロパティの変更を監視するために使用されます。
ファイル core/instance/lifecycle.js には、$forupdate、$destroy、$forupdate、$destroy などの Vue ライフサイクルに関連する関数が含まれています。インスタンス化 Watcher の mountComponent 関数では、コンポーネントがマウントされて $mount が実行されると、mountComponent 関数がトリガーされます。この関数は、最初に beforeMount フック イベントをトリガーします。Watcher をインスタンス化するときに、before 関数が渡され、before によって beforeUpdate フックがトリガーされます。コンポーネントにプロパティの更新がある場合、ウォッチャーは更新前に beforeUpdate イベントをトリガーします (watcher.run)。 isRenderWatcher は、レンダリング ウォッチャーが作成され、vm._watcher 属性に直接ハングされることを示します。レンダリングを更新するために $forceUpdate が強制的に実行されると、vm._watcher.update が実行されて、レンダリング プロセスと対応する更新フックがトリガーされます。
/** * 生命周期mount事件触发函数 * @param {*} vm * @param {*} el * @param {*} hydrating * @returns */ export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el callHook(vm, 'beforeMount') let updateComponent = () => { vm._update(vm._render(), hydrating) } // 实例化Watcher对象,在Watcher构造函数中建立Watcher和vm的关系 new Watcher(vm, updateComponent, noop, { // 在执行wather.run函数之前触发before hook事件 before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } // isRenderWatcher表示用于渲染的Watcher,在执行$forceupdate时会手动触发watcher.update }, true /* isRenderWatcher */) return vm } export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) this.getter = expOrFn this.value = this.lazy ? undefined : this.get() } } Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } }
コンポーネントでは、watch メソッドと計算メソッドを使用してプロパティの変更を監視することに加えて、Vue はプロパティの変更を監視する $watch 関数を定義します。 a.b.c ネストされたプロパティ 変更の場合、$watch を使用して監視と後続の処理を実装できます。$watch は、コンポーネントに watch 属性を直接記述する機能的な方法と同等です。実行時に依存監視の動的な追加をサポートできます。たとえば、 Vue ソース コードのキープアライブ コンポーネントがマウントされている イベントで $watch を使用して、属性の変更を監視します。
vm.$watch( expOrFn, callback, [options] ) 参数: {string | Function} expOrFn {Function | Object} callback {Object} [options] {boolean} deep {boolean} immediate 返回值:{Function} unwatch // 键路径 vm.$watch('a.b.c', function (newVal, oldVal) { // 做点什么 }) // keep-alive.js文件 mounted () { this.cacheVNode() this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }
$watch 関数と mountComponent 関数の違いは、mountComponent はレンダリングの監視に使用され、関連するフック イベントをトリガーするのに対し、$watch はより具体的な責任を持ち、expOrFn の監視を処理することです。さらに、$watch の cb パラメータには、関数、オブジェクト、または文字列を指定できます。文字列の場合は、Vue オブジェクトで定義された関数名を意味します。たとえば、nameChange 関数が Vue コンポーネントで定義されている場合、 define vm.$watch('name' , 'nameChange')、名前が更新されると、Vue エンティティの nameChange 関数がトリガーされます。
// 监听属性变化 Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this // cb可能是纯JS对象,那么回调为cb.handler if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } const watcher = new Watcher(vm, expOrFn, cb, options) // 返回watch注销监听函数 return function unwatchFn () { watcher.teardown() } } function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { // 当执行函数是一个对象的时候, 将 handler 的 handler调用给执行函数 // 这里的 options 是 watch 函数的配置信息 if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) }
Vue を使用してコンポーネントを開発します。これら 2 つのプロパティはよく知っている必要があります。たとえば、watch を使用して firstName プロパティと SecondName プロパティの監視を定義し、次を使用します。 fullName プロパティの監視を定義するために計算されます。firstName と SecondName が更新されると、fullName も更新をトリガーします。
new Vue({ el: '#app', data() { return { firstName: 'Li', secondName: 'Lei' } }, watch: { secondName: function (newVal, oldVal) { console.log('second name changed: ' + newVal) } }, computed: { fullName: function() { return this.firstName + this.secondName } }, mounted() { this.firstName = 'Han' this.secondName = 'MeiMei' } })
watch および computed でプロパティのモニタリングを定義する場合、Vue はそれを Watcher オブジェクトに変換してモニタリングを実行するのはいつですか? Vue のコンストラクターは、初期化を実行するために _init(options) を呼び出します。ソース コード core/components/instance/init.js ファイルは、_init 関数を定義し、初期化ライフ サイクル、イベント、ステータスなどの一連の初期化操作を実行します。この関数には、監視と計算の初期化が含まれます。
// core/components/instance/init.js // Vue构造函数 function Vue (options) { this._init(options) } // core/components/instance/init.js Vue.prototype._init = function (options?: Object) { initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') } // // core/components/state.js export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options ... if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
initComputed は計算属性を初期化します。各 Vue エンティティには、すべての計算属性のウォッチャー オブジェクトを格納するために使用される _computedWatchers オブジェクトが含まれています。まず、計算されたオブジェクトを走査し、キーごとに新しい Watcher オブジェクトを作成します。その遅延属性は true です。これは、Watcher が計算値をキャッシュすることを意味します。依存するプロパティ (firstName、secondName など) が更新されない場合、現在計算されたプロパティ (fullName など) は更新をトリガーしません。 computed で定義されたプロパティは、これを通じてアクセスできます (例: this.fullName)。defineComputed は、すべての計算されたプロパティを Vue エンティティにマウントします。
// lazy为true表示需要缓存,一般只有computed属性才会用到 const computedWatcherOptions = { lazy: true } function initComputed (vm: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = computed[key] // 用户定义的执行函数可能是{ get: function() {} }形式 const getter = typeof userDef === 'function' ? userDef : userDef.get // 为用户定义的每个computed属性创建watcher对象 watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) // 组件自身的computed属性已经定义在组件原型链上,我们只需要定义实例化的computed属性。 // 例如我们在computed定义了fullName,defineComputed会将其挂接到Vue对象的属性上 if (!(key in vm)) { defineComputed(vm, key, userDef) } }
defineComputed 関数は、計算されたプロパティを {get, set} 形式に変換しますが、計算されたプロパティは set する必要がないため、コードは noop の空の関数を直接割り当てます。計算された属性の get 関数は、createComputedGetter によってカプセル化されます。まず、対応する属性のウォッチャー オブジェクトが見つかります。ウォッチャーのダーティ値が true の場合、依存属性が更新されたことを意味し、評価関数は次のことを行う必要があります。新しい値を再計算するために呼び出されます。
// 将computed定义的属性转换为{ get, set }形式并挂接到Vue实体上,这样就可以通过this.fullName形式调用 export function defineComputed ( target: any, key: string, userDef: Object | Function ) { if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? createComputedGetter : noop sharedPropertyDefinition.set = userDef.set || noop } Object.defineProperty(target, key, sharedPropertyDefinition) } // 定义computed的专属getter函数 function createComputedGetter (key) { return function computedGetter () { // _computedWatchers上为每个computed属性定义了Watcher对象 const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // dirty为true,表示依赖的属性有变化 if (watcher.dirty) { // 重新计算值 watcher.evaluate() } if (Dep.target) { // 将Dep.target(watcher)附加到当前watcher的依赖中 watcher.depend() } return watcher.value } } }
如果Dep.target有值,将其他依赖当前计算属性的Watcher(例如使用到fullName的依赖Watcher)附加到当前计算属性所依赖的属性的dep集合中。如下面的代码创建了对fullName计算属性的监听, 我们将其命名为watcher3。那么firstName和secondName的dep对象都会附加上watcher3观察者,只要其属性有任何变化,都会触发watcher3的update函数,重新读取fullName属性值。
vm.$watch('fullName', function (newVal, oldVal) { // 做点什么 })
initWatch函数逻辑相对简单些,遍历每个属性的依赖项,如果依赖项为数组,则遍历数组,为每个依赖项单独创建Watcher观察者,createWatcher函数在前文中有提到,它使用$watch创建新的watcher实体。
// 初始化Watch属性 function initWatch (vm: Component, watch: Object) { for (const key in watch) { const handler = watch[key] // 如果对应属性key有多个依赖项,则遍历为每个依赖项创建watcher if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }
Vue在core/observer/scheduler.js文件定义了调度函数,一共有两处使用,Watcher对象以及core/vdom/create-component.js文件。watcher对象在执行更新时,会被附加到调度队列中等待执行。create-component.js主要处理渲染过程,使用scheduler的主要作用是触发activated hook事件。这里重点阐述Watcher对Scheduler的使用。
当执行watcher的update函数,除了lazy(计算属性watcher)、sync(同步watcher),所有watcher都将调用queueWatcher函数附加到调度队列中。
export default class Watcher { /** * 通知订阅,如果依赖项有更新,该函数会被触发 */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } }
queueWatcher函数定义如下,函数的目的是将watcher附加到调度队列中,对调度队列创建微任务(microTask),等待执行。关于microTask和macroTask的区别,看查看参考8“宏任务macroTask和微任务microTask的区别”。如果微任务flushSchedulerQueue还未执行(flushing为false),直接将watcher附加到queue即可。否则,还需判断当前微任务的执行进度,queue会按watcher的id做升序排序,保证先创建的watcher先执行。index为微任务中正在被执行的watcher索引,watcher将会插入到大于index且符合id升序排列的位置。最后队列执行函数flushSchedulerQueue将通过nextTick创建一个微任务等待执行。
/* * 附加watcher到队列中,如果有重复的watcher直接跳过。 * 如果调度队列正在执行(flushing为true),将watcher放到合适的位置 */ export function queueWatcher (watcher: Watcher) { // 所有watcher都有一个递增的唯一标识, const id = watcher.id // 如果watcher已经在队列中,不做处理 if (has[id] == null) { has[id] = true if (!flushing) { // 如果队列还未执行,则直接附加到队列尾部 queue.push(watcher) } else { // 如果正在执行,基于id将其附加到合适的位置。 // index为当前正在执行的watcher索引,并且index之前的watcher都被执行了。 // 先创建的watcher应该被先执行,和队列中的watcher比较id大小,插入到合适的位置。 let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } // i的位置,表明 watcher[i - 1].id < watcher[i].id < watcher[i + 1].id queue.splice(i + 1, 0, watcher) } // 如果未排队,开始排队,nextick将执行调度队列。 if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }
nextTick将会选择适合当前浏览器的微任务执行队列,例如MutationObserver、Promise、setImmediate。flushSchedulerQueue函数将遍历所有watcher并执行更新,首先需要将queue做升序排序,确保先创建的watcher先被执行,例如父组件的watcher优先于子组件执行。接着遍历queue队列,先触发watcher的before函数,例如前文中介绍mountComponent函数在创建watcher时会传入before事件,触发callHook(vm, 'beforeUpdate')。接下来就具体执行更新(watcher.run)操作。当队列执行完后,调用resetSchedulerState函数清空队列、重置执行状态。最后callActivatedHooks和callUpdatedHooks将触发对应的activated、updated hook事件。
/** * 遍历执行所有的watchers */ function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // 遍历之前先排序队列 // 排序的队列能确保: // 1.父组件先于子组件更新,因为父组件肯定先于子组件创建。 // 2.组件自定义的watcher将先于渲染watcher执行,因为自定义watcher先于渲染watcher创建。 // 3.如果组件在父组件执行wtcher期间destroyed了,它的watcher集合可以直接被跳过。 queue.sort((a, b) => a.id - b.id) // 不要缓存length,因为在遍历queue执行wacher的同时,queue队列一直在调整。 for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { // 通过before可触发hook,例如执行beforeUpdated hook watcher.before() } id = watcher.id has[id] = null // 执行watcher的更新 watcher.run() } // 由于activatedChildren和queue两个队列一直在更新,因为需要拷贝处理 const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() // 重置掉队队列状态 resetSchedulerState() // 触发activated和updated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) }
调度队列会执行watcher的run函数触发更新,每个watcher有active状态,表明当前watcher是否处于激活状态,当组件执行$destroy函数,会调用watcher的teardown函数将active设置为false。在执行更新通知回调cb之前,有三个条件判断,首先判断值是否相等,对于简单值string或number类型的可直接判断;如果value为对象或需要深度遍历(deep为true),例如用户自定义了person属性,其值为对象{ age: number, sex: number },我们使用$watch('person', cb)监听了person属性,但当person.age发生变化时,cb不会被执行。如果改成$watch('person', cb, { deep: true }),任何嵌套的属性发生变化,cb都会被触发。满足三个条件其中之一,cb回调函数将被触发。
export default class Watcher { /** * 调度接口,将被调度器执行 */ run () { // 仅当watcher处于激活状态,才会执行更新通知 // 当组件destroyed时,会调用watcher的teardown将其重置到非激活状态 if (this.active) { // 调用get获取值 const value = this.get() if ( // 如果新计算的值更新了 value !== this.value || // 如果value为对象或数组,不管value和this.value相等否,则其深度watchers也应该被触发 // 因为其嵌套属性可能发生变化了 isObject(value) || this.deep ) { const oldValue = this.value this.cb.call(this.vm, value, oldValue) } } } } this.$watch('person', () => { this.message = '年龄为:' + this.person.age }, // 当deep为true,当age更新,回调会被触发;如果deep为false,age更新不会触发回调 { deep: true } )
run函数有调用get获取最新值,在get函数中,首先调用pushTarget函数将当前Watcher附加到全局Dep.target上,然后执行getter获取最新值。在finally模块中,如果deep为true,则调用traverse递归遍历最新的value,value可能为Object或者Array,所以需要遍历子属性并触发其getter函数,将其dep属性附加上Dep.target(当前Watcher),这样任何子属性的值发生变化都会通知到当前watcher,至于为什么,可以回顾下上篇《Vue如何实现数据状态的侦测》。
export default class Watcher { /** * 执行getter,重新收集依赖项 */ get () { // 将当前Watcher附加到全局Dep.target上,并存储targetStack堆栈中 pushTarget(this) let value const vm = this.vm try { // 执行getter读取value value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // 如果deep为true,将遍历+递归value对象 // 将所有嵌套属性的dep都附加上当前watcher,所有子属性对应的dep都会从push(Dep.target) if (this.deep) { // 递归遍历所有嵌套属性,并触发其getter,将其对应的dep附加当前watcher traverse(value) } // 退出堆栈 popTarget() // 清理依赖 this.cleanupDeps() } return value } }
在get函数中为什么要执行traverse递归遍历子属性,我们可以通过实际的例子来说明,例如在data中定义了{ person: { age: 18, sex: 0, addr: { city: '北京', detail: '五道口' } }, Vue会调用observe将person转换为如下Observer对象,子属性(如果为对象)也会转换为Observer对象,简单属性都会定义get、set函数。
当watcher.get执行traverse函数时,会递归遍历子属性,当遍历到addr属性时,触发get函数,该函数将调用其dep.depend将当前Watcher附加到依赖项中,这样我们在执行执行this.person.age = 18,其set函数调用dep.notify触发watcher的update函数,实现person对象的监听。
get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() ... } return value } set: function reactiveSetter (newVal) { ... dep.notify() }
本文首先介绍Watcher从创建到最终组件渲染经历的流程,然后就"何时创建Watcher"、"Scheduler调度处理"、"Watcher更新"三部分作了详细介绍。通过"何时创建Watcher"了解到Vue在哪些地方会创建Watcher对象,"Scheduler调度处理"介绍了Vue如何通过微任务来执行wacher队列的更新,"Watcher更新"简单讲述了每一个watcher如何执行更新。由于篇幅原因,watcher的更新最终如何体现到视图的渲染,将在下一篇做具体介绍。篇幅有点长,能看到这里,说明大家对Vue是真爱。
更多编程相关知识,请访问:编程入门!!
以上がVue のウォッチャーとスケジューラーの詳細については、こちらをご覧ください。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。