Vue watcher 分类 前言 知道 Vue 响应式原理的都知道,每个组件都对应有一个自己的 watcher 实例,但是除了组件对应的 watcher 实例,computed 和 watch 也都分别对应有自己的 watcher 实例,所以下面我们就针对这三种 watcher 做一下研究,知道了底层实现,也能更好的让我们在使用中做出更好的抉择。
Watcher 的初始化发生在 Vue 实例初始化阶段的 initState
函数中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Vue.prototype._init = function (options?: Object ) { const vm: Component = this initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate' ) initInjections(vm) initState(vm) initProvide(vm) callHook(vm, 'created' ) if (vm.$options.el) { vm.$mount(vm.$options.el) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export function initState (vm: Component ) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true ) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
这里先创建了 computed 和 watch 的 watcher 实例,组件的 watcher 实例是在执行 vm.$mount 后创建的,所以这三种 watcher 执行顺序为 computed watcher => user watcher => render watcher。
一、computed watcher 1、数据劫持 计算属性的初始化是发生在 Vue 实例初始化阶段的 initState
函数中。先遍历 computed
对象,为每一个计算属性创建一个 computed watcher 。然后判断如果该计算属性不是 vm
的属性,则调用 defineComputed(vm, key, userDef)
,否则判断计算属性是否已经被 data
或 prop
占用,如果占用则在开发环境报相应警告。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 function initComputed (vm: Component, computed: Object ) { const watchers = vm._computedWatchers = Object .create(null ) const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (process.env.NODE_ENV !== 'production' && getter == null ) { warn( `Getter is missing for computed property "${key} ".` , vm ) } if (!isSSR) { watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production' ) { if (key in vm.$data) { warn(`The computed property "${key} " is already defined in data.` , vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key} " is already defined as a prop.` , vm) } else if (vm.$options.methods && key in vm.$options.methods) { warn(`The computed property "${key} " is already defined as a method.` , vm) } } } }
利用 Object.defineProperty
给每一个计算属性添加 getter 和 setter。setter 通常是计算属性是一个对象,并且拥有 set
方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.set ? userDef.set : noop } if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function ( ) { warn( `Computed property "${key} " was assigned to but it has no setter.` , this ) } } Object .defineProperty(target, key, sharedPropertyDefinition) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function createComputedGetter (key ) { return function computedGetter ( ) { const watcher = this ._computedWatchers && this ._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }
2、依赖收集 当调用 render
函数访问到计算属性的时候,就触发了计算属性的 getter
进行求值,将 dirty
设为 false
,用于缓存 。 getter
会触发依赖的属性去收集 render watcher 和此时的 computed watcher 。
3、依赖更新 一旦我们对计算属性依赖的数据做修改,则会触发 setter ,通知所有订阅它变化的 watcher
更新,包括 render watcher 和 computed watcher 。对于 computed watcher 就是将 dirty
设为 true
。对于 render watcher 会执行 Watcher
实例的 run
方法重新执行组件的render
函数。这样在渲染组件的过程中又会触发计算属性,然后继续对计算属性进行求值,使得依赖的属性继续收集 computed watcher 。
二、user watcher 1、数据劫持 侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState
函数中。遍历 watch
对象,拿到每一个 handler
,因为 Vue 是支持 watch
的同一个 key
对应多个 handler
,所以如果 handler
是一个数组,则遍历这个数组,每一项调用 createWatcher
方法,否则直接调用 createWatcher
。然后创建一个 user watcher 。
1 2 3 4 5 6 7 8 9 10 11 12 function initWatch (vm: Component, watch: Object ) { for (const key in watch) { const handler = watch[key] if (Array .isArray(handler)) { for (let i = 0 ; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string' ) { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { const info = `callback for immediate watcher "${watcher.expression} "` pushTarget() invokeWithErrorHandling(cb, vm, [watcher.value], vm, info) popTarget() } return function unwatchFn ( ) { watcher.teardown() } }
2、依赖收集 当初始化试图或更新试图访问属性的 getter 方法时,会触发属性去收集 render watcher 和对应的的 user watcher 。
3、依赖更新 一旦我们对数据做修改,则会触发 setter ,通知所有订阅它变化的 watcher
更新,包括 render watcher 和 user watcher 。
4、其他特性 4.1、deep 如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true。
这个时候是不会 log 任何数据的,因为我们是 watch 了 a
对象,只触发了 a
的 getter,并没有触发 a.b
的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2
赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var vm = new Vue({ data() { a: { b: 1 } }, watch: { a: { handler(newVal) { console .log(newVal) } } } }) vm.a.b = 2
而我们只需要对代码做稍稍修改,就可以观测到这个变化了
1 2 3 4 5 6 7 8 watch: { a: { deep: true , handler(newVal) { console .log(newVal) } } }
这样在 watcher
执行 get
求值的过程中有一段逻辑,traverse 实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher。
1 2 3 4 5 6 7 get () { let value = this .getter.call(vm, vm) if (this .deep) { traverse(value) } }
三、render watcher 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate' ) } } }, true ) }
详情见 Vue 响应式原理
四、问题 1、computed 和 watch 的区别
computed: 依赖的属性值发生变化才会重新计算 computed 的值;
watch: 监听的数据发生变化就会执行回调进行后续操作;
微信打赏