前言 知道 Vue 响应式原理的都知道,每个组件都对应有一个自己的 watcher 实例,但是除了组件对应的 watcher 实例,computed 和 watch 也都分别对应有自己的 watcher 实例,所以下面我们就针对这三种 watcher 做一下研究,知道了底层实现,也能更好的让我们在使用中做出更好的抉择。
Watcher 的初始化发生在 Vue 实例初始化阶段的 initState 函数中。
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 ) } }
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 占用,如果占用则在开发环境报相应警告。
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 的情况比较少。
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) }
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 。
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) } } }
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) }
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 的回调函数了。
var vm = new Vue ({ data ( ) { a : { b : 1 } }, watch : { a : { handler (newVal ) { console .log (newVal) } } } }) vm.a .b = 2
而我们只需要对代码做稍稍修改,就可以观测到这个变化了
watch : { a : { deep : true , handler (newVal ) { console .log (newVal) } } }
这样在 watcher 执行 get 求值的过程中有一段逻辑,traverse 实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher。
get ( ) { let value = this .getter .call (vm, vm) if (this .deep ) { traverse (value) } }
三、render watcher 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: 监听的数据发生变化就会执行回调进行后续操作;