Vue2.x中watch的内部原理主要也是应用了响应式原理中不可缺少的一部分watcher,只不过这个watcher区别于之前讲述的watcher,这个是用户自定义的
userWatcher
,下面来分析下侦听属性watch整体实现的过程:
1 | function initWatch (vm: Component, watch: Object) { |
Vue先会遍历所有定义在watch中的属性,而属性的值可以是数组(上篇文章提到的当一个数据需要多次监听),如果是数组,则再进一步遍历,然后调用createWatcher
方法,在createWatcher
方法中会做一些Object、String的判断,最后会调用$watch方法,并传入相关的参数。
$watch源码:
1 | Vue.prototype.$watch = function ( |
从源码上来看,$watch的实现也很简单,首先会判断传入的handler
也就是上面的cb
是不是Object,如果是的话,会递归调用createWatcher
方法,正常在开发过程中watch的值的回调函数就是这里的cb
,最终会根据相关的参数new一个新的watcher
,而这个watcher
是一个userWatcher
,因为有options.user = true
这段代码。这样就初始化好了一个userWatcher
watcher的相关源码:
1 | update () { |
当watch的数据有变化的时候,会执行Object.defineProperty
的set方法,使得订阅者Dep
通知所有存储在其中的watcher,并执行其中的update
方法,update
会走到queueWatcher(this)
,最终会走到watcher的run
方法(内部实现复杂,涉及到nextTick
,但最终还是会走入到run
方法中),接着进入到getAndInvoke
方法中。
分析一下getAndInvoke
方法,首先会拿到新的值,并与之前的值进行判断,如果不一样,则进入到下面的逻辑中。因为在之前已经定义了是个userWatcher
,所以会进入到if
中,执行cb.call(this.vm, value, oldValue)
,这里的cb
就是我们定义的回调函数,它改变this指向到当前组件,同时将新的value
和旧的value
传入(这也就是我们能在方法中获取到新旧值的原因),然后执行这个回调函数。
经过以上分析,侦听属性watch的整体过程还是相对简单的。下面根据一个具体例子来说一下过程:
1 | <template> |
1、初始化的时候,先用Object.defineProperty
转换message
,之后会通过上述所讲的初始化一个message
的userWatcher
,而这里定义在watch中的回调函数将作为handler
传入,也就是后面的cb
2、当点击按钮的时候,message
发生了改变,会调用其中的set方法,从而走入到userWatcher
中的update
方法中
3、update
最后进入到getAndInvoke
中,首先会拿到新值,然后对比旧值,因为hello
与world
不相等,继续执行
4、因为这个watcher是一个userWatcher
,所以会进入到内层的if中,执行cb.call(this.vm, value, oldValue)
5、此时的this.vm
是当前的组件,value
是world
对应回调函数中的参数newVal
,oldValue
是hello
对应回调函数中的参数oldVal
,所以可以将两个值console
出
扩展
在定义侦听属性的时候,可以传入一些参数,deep
、sync
、immediate
immediate
从刚开始给出的源码中可以看出,在new一个userWatcher
之后,有个条件判断options.immediate
,如果为真,则执行cb.call(vm, watcher.value)
,watcher.value
即获取监听属性的值,所以,如果设置immediate为true,就是在初始化的时候执行一次,会马上获取到监听属性的值,对应刚才的例子,则是获取到hello
sync
在上面最后给的一段源码中的update
方法中,可以找到sync
的判断条件,会执行run
方法,走之后的逻辑,其实设置了这个参数为true的时候,将不会走queueWatcher
中,也就是说不会涉及到nextTick
(nextTick
是个异步的过程),最终会同步执行cb,所以只有在watch的值的变化到执行watcher的回调函数是一个同步过程的时候才会去设置该属性为 true
deep
设置deep: true
是为了观察到深层的变化,可以在watcher中的get方法中看出,下面是get方法的源码:
1 | get () { |
当deep
为true
时,执行traverse(value)
,其实traverse
的内部则是进一步遍历,直到不能在遍历为止
所以,当要观察的属性是对象、数组等值时,则需要设置该属性,因为正常引用类型的对比是对比内存地址。但是,如果是观察的是基本类型的值,不要设置,因为深层遍历会耗性能,虽然Vue已经做了优化,但只是在做判断,还是会执行一些没必要的代码,所以还是不要设置的好。
总结
以上就是有关于watch的所有内容,整个watch的过程相对来说并不复杂,但内部也有很多需要注意的地方,了解了内部的原理对正常的开发会有很大的帮助。