Rumah >hujung hadapan web >View.js >Ketahui lebih lanjut tentang Pemerhati dan Penjadual dalam Vue
Artikel ini akan membawa anda memahami Pemerhati dan Penjadual dalam Vue, dan memperkenalkan prinsip pelaksanaan Pemerhati Vue, saya harap ia akan membantu anda.
Vue merasakan keadaan berubah melalui mekanisme pengesanan data "Cara Vue Melaksanakan Pengesanan Data" menyebut objek Watcher Apabila data dikemas kini, terdapat kemas kini , seperti Apabila melaksanakan this.title = 'Lihat sama ada saya telah berubah', fungsi setter memanggil dep.notify untuk memberitahu pemerhati untuk melakukan kemas kini (khususnya melaksanakan fungsi watcher.update).
Jadi bilakah Vue membuat Pemerhati, cara menjadualkan baris gilir Pemerhati melalui Penjadual, dan cara kemas kini pemerhati akhirnya ditunjukkan dalam pemaparan paparan Artikel ini memfokuskan kepada tiga isu ini memperkenalkan prinsip pelaksanaan Vue's Watcher. [Cadangan berkaitan: "Tutorial vue.js"]
daripada penciptaan hingga kemusnahan Selepas melalui satu siri kitaran hayat, kitaran yang lebih kita kenali ialah beforeMount, mounted, beforeUpdate, dan dikemas kini Selepas memahami kitaran hayat, ia akan menjadi lebih mudah untuk difahami apabila Watcher dicipta. Vue mencipta objek Watcher di tiga tempat, acara pelekap, fungsi $watch, sifat dikira dan menonton Acara pelekap mencipta Pemerhati untuk membuat pemberitahuan yang dibuat oleh jam tangan dan dikira digunakan untuk memantau perubahan sifat yang ditentukan pengguna.
Inti fail/instance/lifecycle.js mengandungi fungsi yang berkaitan dengan kitaran hayat Vue, seperti $forupdate, $destroy dan instantiation Fungsi mountComponent Watcher, fungsi mountComponent dicetuskan apabila komponen dipasang dan $mount dilaksanakan. Apabila komponen mempunyai kemas kini sifat, pemerhati akan mencetuskan acara beforeUpdate sebelum mengemas kini (watcher.run). isRenderWatcher menunjukkan bahawa Pemerhati rendering dibuat dan digantung terus pada atribut vm._watcher Apabila $forceUpdate dilaksanakan secara paksa untuk menyegarkan pemaparan, vm._watcher.update akan dilaksanakan untuk mencetuskan proses pemaparan dan cangkuk kemas kini yang sepadan.
/** * 生命周期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() } }
Dalam komponen, selain menggunakan jam tangan dan kaedah pengiraan untuk memantau perubahan harta benda, Vue mentakrifkan fungsi $watch untuk memantau perubahan harta, seperti apabila a.b.c dibenamkan Untuk menetapkan perubahan atribut, anda boleh menggunakan $watch untuk melaksanakan pemantauan dan pemprosesan seterusnya $watch adalah bersamaan dengan cara berfungsi untuk menulis atribut jam tangan secara langsung dalam komponen. seperti komponen keep-alive dalam kod sumber Vue Gunakan $watch dalam acara yang dipasang untuk memantau termasuk dan mengecualikan perubahan atribut.
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)) }) }
Perbezaan antara fungsi $watch dan fungsi mountComponent ialah mountComponent digunakan untuk pemantauan pemaparan dan akan mencetuskan peristiwa cangkuk yang berkaitan, manakala $watch mempunyai tanggungjawab yang lebih khusus dan mengendalikan pemantauan expOrFn. Di samping itu, parameter cb $watch boleh menjadi fungsi, objek atau rentetan Apabila ia adalah rentetan, ia bermakna nama fungsi yang ditakrifkan dalam objek Vue Contohnya, jika fungsi namaChange ditakrifkan dalam komponen Vue define vm.$watch('name' , 'nameChange'), jika nama dikemas kini, fungsi nameChange entiti Vue akan dicetuskan.
// 监听属性变化 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) }
Gunakan Vue untuk membangunkan komponen ini. Contohnya, gunakan jam tangan untuk mentakrifkan pemantauan atribut firstName dan secondName. dan gunakan computed untuk mentakrifkan atribut FullName, apabila firstName dan secondName dikemas kini, fullName juga akan mencetuskan kemas kini.
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' } })
Apabila kami mentakrifkan pemantauan sifat dalam jam tangan dan dikira, bilakah Vue menukarnya menjadi objek Pemerhati untuk melaksanakan pemantauan? Pembina Vue akan memanggil _init(options) untuk melakukan pengamulaan Fail teras/komponen/instance/init.js kod sumber mentakrifkan fungsi _init dan melaksanakan satu siri operasi pengamulaan, seperti kitaran hayat permulaan, peristiwa, status, dsb., antaranya initState Fungsi termasuk pengamulaan jam tangan dan dikira.
// 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 memulakan atribut yang dikira Setiap entiti Vue mengandungi objek _computedWatchers untuk menyimpan objek pemerhati untuk semua atribut yang dikira. Mula-mula melintasi objek yang dikira dan mencipta objek Pemerhati baharu untuk setiap kekunci Atribut malasnya adalah benar, yang bermaksud bahawa Pemerhati akan menyimpan nilai yang dikira Jika sifat yang bergantung padanya (seperti Nama pertama, Nama kedua) tidak dikemas kini harta pengiraan semasa (seperti Nama penuh) ) tidak akan mencetuskan kemas kini. Sifat yang ditakrifkan dalam pengiraan boleh diakses melalui ini (contohnya, this.fullName defineComputed memasang semua sifat yang dikira ke entiti 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) } }
Fungsi defineComputed menukar atribut yang dikira ke dalam bentuk {get, set}, tetapi atribut yang dikira tidak memerlukan set, jadi kod tersebut memberikan fungsi kosong noop kepadanya secara langsung. Fungsi get bagi atribut yang dikira dirangkumkan oleh createComputedGetter Pertama, objek pemerhati atribut yang sepadan ditemui Jika nilai kotor pemerhati adalah benar, ini bermakna atribut bergantung telah dikemas kini dan fungsi menilai perlu. dipanggil untuk mengira semula nilai baharu.
// 将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是真爱。
更多编程相关知识,请访问:编程入门!!
Atas ialah kandungan terperinci Ketahui lebih lanjut tentang Pemerhati dan Penjadual dalam Vue. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!