看原理解析前,先把 $watch 的看一下。
vm.$watch API 的作用很简单,就是对一个目标进行监控,一旦该目标变化了的话,就会触发注册的回调函数,接下来开始看源码。
export function stateMixin (Vue: Class<Component>) {
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
// vm.$watch 方法的核心,借助 Watcher 实现功能
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
// 如果 immediate 为 true 的话,立即执行回调函数
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
}
$watch 方法的内容很简单,因为实现 $watch 功能的代码主要在 Watcher 类里面,$watch 方法中的代码主要进行一些参数的处理和附加功能的实现。
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
我们知道,$watch API 有两种写法:
this.$watch('numA',() => {
console.log('numA 改变了')
},{
immediate: true
})
this.$watch('numB',{
handler(){
console.log('numB 改变了')
},
immediate: true
})
第一种写法:第一个参数是监控的目标,第二个参数是回调函数,第三个参数是配置对象;
第二种写法:第一个参数是监控的目标,第二个参数是一个对象,对象中的 handler 函数是对应的回调函数,配置对象也写在这个对象中;
$watch 中的第一段代码就是用于处理第二种写法的,将第二种写法转换成第一种写法,确保执行到下面的代码时,参数的形式只能是第一种,我们看下 createWatcher() 的源码。
function createWatcher (
vm: Component,
keyOrFn: string | Function,
handler: any,
options?: Object
) {
// 如果 handler 是对象类型的话,需要进行下数据整形,确保 handler 指向处理函数,options 指向配置对象
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 如果 handler 是字符串类型的话,从 vm 实例中获取到对应的处理函数
if (typeof handler === 'string') {
handler = vm[handler]
}
// 调用 vm.$watch 实现侦听属性的功能
// 代码执行到这里,handler 只能是函数类型的
return vm.$watch(keyOrFn, handler, options)
}
在这里,handler 是一个对象,里面包含 handler 回调函数和配置信息,需要把他们分离开并存储到对应的变量中。
接下来,如果 handler 是一个字符串的话,说明 handler 函数是注册在 Vue 实例中的一个方法,通过 vm[handler] 获取并赋值给 handler。
if (options.immediate) {
// 如果 immediate 为 true 的话,立即执行回调函数
cb.call(vm, watcher.value)
}
这段代码是用于处理 immediate 配置参数的,如果配置的 immediate 属性为 true 的话,则立即执行 cb 回调函数,回调函数中的 this 指向 vm(当前的组件实例),传递的参数是当前 $watch 监控目标的值。
return function unwatchFn () {
watcher.teardown()
}
通过官方文档可知,$watch API 最后会返回一个函数,执行该函数,会解除对目标的监控。内部的实现是调用了 watcher.teardown() 方法,该方法的内部实现也很简单,只是将自己(Watcher 实例)从存储的 Dep 实例中移除掉,具体内容下面再说。
接下来,看最重要的一行代码:
// vm.$watch 方法的核心,借助 Watcher 实现功能
const watcher = new Watcher(vm, expOrFn, cb, options)
我们以下面的业务代码为例:
<template>
<div id="app">
<h1>{{person.age}}岁了</h1>
<button @click="changeAge">change age</button>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
person: {
age: 1
}
}
},
mounted() {
this.$watch('person.age',function ageChange() {
console.log('年龄改变了')
})
},
methods: {
changeAge(){
this.person.age++
},
}
}
</script>
如果用户点击了 change age 按钮的话,age 属性便会变化,然后 ageChange 回调函数便会触发执行。
在这个例子中,new Watcher(vm, expOrFn, cb, options) 中的 expOrFn 是 'person.age',cb 对应 ageChange 回调函数,接下来我们开始看 Watcher 类的源码。
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
this.cb = cb
// getter 属性必须是一个函数,并且函数中有对使用到的值的读取操作(用于触发数据的 getter 函数,在 getter 函数中进行该数据依赖的收集)
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 而如果是一个字符串类型的话,例如:"a.b.c.d",是一个数据的路径
// 就将 parsePath(expOrFn) 赋值给 this.getter,
// parsePath 能够读取这个路径字符串对应的数据(一样能触发 getter,触发数据的 getter 是关键)
this.getter = parsePath(expOrFn)
}
this.value = this.get()
}
get () {
// 将自身实例赋值到 Dep.target 这个静态属性上(保证全局都能拿到这个 watcher 实例),
// 使得 getter 函数使用数据的 Dep 实例能够拿到这个 Watcher 实例,进行依赖的收集。
// pushTarget 操作很重要
pushTarget(this)
let value
const vm = this.vm
try {
// 执行 getter 函数,该函数执行时,会对响应式的数据进行读取操作,这个读取操作能够触发数据的 getter,
// 在 getter 中会将 Dep.target 这个 Watcher 实例存储到该数据的 Dep 实例中,以此就完成了依赖的收集
// 依赖收集需要执行 addDep() 方法完成
value = this.getter.call(vm, vm)
} catch (e) {
......
}
// 将 expOrFn 对应的值返回出去
return value
}
}
在我们的例子中,expOrFn 的值是 'person.age',所以会进入 this.getter = parsePath(expOrFn) 的逻辑。Watcher 中的 getter 属性必须是一个函数,并且调用这个函数要能够访问并返回目标值,这个访问的动作很关键,因为要触发数据的 getter。所以 parsePath 方法的作用是将 'person.age',转换成如下的函数:
const segments = ['person','age']
function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
get 方法中执行了 pushtarget(this) 和 value = this.getter.call(vm, vm),这两条语句前面的博客已经说了,具体原理就不解释了,作用是:进行依赖收集,使 vm.person.age 这个属性对应的 Dep 实例存储当前的 Watcher 实例。
数据发生变化,对应的 setter 方法会被触发执行。
// 在此进行派发更新
set: function reactiveSetter (newVal) {
// 触发依赖的更新
dep.notify()
}
setter 方法中会执行该数据对应 Dep 实例的 notify() 方法。
// 触发 subs 数组中依赖的更新操作
notify () {
// 数组的 slice 函数具有拷贝的作用
const subs = this.subs.slice()
// 遍历 subs 数组中的依赖项
for (let i = 0, l = subs.length; i < l; i++) {
// 执行依赖项的 update 函数,触发执行依赖
subs[i].update()
}
}
notify 方法会遍历执行 subs 数组中 Watcher 实例的 update 方法
update () {
this.run()
}
run () {
if (this.active) {
const value = this.get()
// 下面进行回调函数 cb 的处理
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
首先调用 this.get() 获取最新的值,然后使用 oldValue 存储旧的值。这样,新的值和旧的值我们都获取到了,最后以这两个值为参数触发执行回调函数。
至此,$watche API 的整个流程就讲完了。
因篇幅问题不能全部显示,请点此查看更多更全内容