七、侦听属性
在initState函数中,如果判断有watch,那么会执行initWatch(vm, opts.watch)方法,initWatch函数中会调用createWatcher(vm, key, handler),key是watch的key,handler为传入的回调函数。createWatcher函数会先判断handler是否是一个对象,如果是一个对象说明我们写入了watch中的handler属性,并且有可能写入了deep或者immediate或sync属性,那么让handler等于传入的handler.handler,如果传入的是一个字符串,那么他会去当前的vm实例上找,这也是handler既可以直接写一个回调函数,也可以让watch触发之后的回调去执行vm上的methods,最后调用了vm.$watch(expOrFn, handler, options),expOrFn实际是监听的数据的key,handler是回调函数,options放入了之前可能定义的熟悉,$watch是实现watch的核心,他绑定在了vue.prototype上提供了直接使用的实例方法,$watch方法会首先判断传入的cb是否是一个对象,如果是一个对象,那么可能是使用者通过实例方法在调用,他会交给之前的函数createWatcher经过和刚才相同的处理,再回到$watch方法,接着定义了options.user为true,然后通过const watcher = new Watcher(vm, expOrFn, cb, options)去new一个user watcher,然后判断他是否写入了属性immediate,如果是立即触发的user watcher那么他会执行cb.call(vm, watcher.value),最终返回unwatchFn函数。也就是说如果通过this.$watch方法创建的user watch最终会返回一个卸载函数。
// src/core/instance/state.js
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) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
在创建user watcher的时候会执行new Watcher(vm, expOrFn, cb, options),expOrFn为监听的属性key,cb为监听到变化之后触发的回调函数,options为传入的配置,其中user 为true。deep,sync根据options传入的deep进行赋值,接着他会判断expOrFn的类型是否是函数,此时传入的expOrFn为一个字符串所以走下边的逻辑,首先他会执行this.getter = parsePath(expOrFn),parsePath函数首选通过split('.')对传入的key进行解析,最终返回一个函数,这个函数根据传入的obj,再通过之前的解析的路径进行寻找,如果找到那么进行返回,如果没有找到那么返回空,那么this.getter等于返回的这个函数,最后会去执行this.get()。进行get函数之后,通过调用pushTarget,让当前dep的target指向当前的user watcher,然后通过 value = this.getter.call(vm, vm)进行之前定义好getter函数的执行,传入的参数是当前的vm实例,这样通过getter函数的调用访问到当前监听的数据,被监听的数据触发依赖收集,此时的dep.target就是当前的user watcher所以形成了依赖关系,当监听的数据发生了更新,则会触发user watcher的update函数中的queueWatcher,也就是flushSchedulerQueue,flushSchedulerQueue函数当中会触发watcher.run(),run函数首先会执行user wathcer 的get函数,和之前分析的一样,他会绑定依赖关系等等步骤,最终get函数会return出拿到的value,也就是数据更新后的值,之后他会拿新的value和旧的value进行比对,如果不相等,或value是个对象,或者设置了deep属性,当前的user为true,所以会执行 this.cb.call(this.vm, value, oldValue);也就是之前传入的回调函数。
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object
){
this.vm = vm
...
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
...
this.sync = !!options.sync
...
} else {
...
}
...
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
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
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
最后如果你传入的属性是一个deep:true,那么在执行watcher get 函数的时候,会通过traverse进行一个处理,traverse函数是对当前的value进行了深度的遍历,也就是让当前的user watcher订阅了其中的每一个属性的更新。如果你传入的属性是一个immediate:true,那么当$watch函数中new watcher之后会执行一次回调函数cb.call(vm, watcher.value)。最后如果传入的sync是true,那么他会优先执行当前user watcher的run方法,也就是说,会优先触发监听,这个地方是当你两个user watcher同时触发,由于在创建的过程中定义的id,在之后的watcher的sort中,先创建的会被先执行,如果希望后创建的先执行回调,那么可以通过传入sync属性,来调整回调函数触发的优先级。
get () {
pushTarget(this)
let value
const vm = this.vm
try {
...
} catch (e) {
...
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
...
}
return value
}
...
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
...
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
...
}
}
...
}
...
update () {
/* istanbul ignore else */
if (this.lazy) {
...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}