源自:https://juejin.im/post/6866090889920872455
背景
最近公司一位部门小妹妹@了我
小妹妹:我的watch监听不到属性,是哪里写错了吗? 但是明明感觉写的没错呀?
心想:Vue的watch又不是什么难的东西,嗯,来机会了,可以让我树立一个完美的形象了,哈哈哈哈...
装作淡定的回道: 你把代码截图发我看看
看了代码之后。。。。。 瞬间就不淡定了, 我看不出有啥问题啊
代码如下:
监听数组的第一个元素的对象中某个属性不就是这样写的吗'zndsList[0].isLoad' () {}
应该不会错的,应该是部门小妹修改数据的事件没有触发,对,肯定是了。
于是我淡定的回了句:你在修改的方法里面alert()看看有没有触发修改的方法,这应该是没有触发值的修改所以才没有触发watch的(心想,嗯,搞定,so easy...)。
谁知部门小妹发来了张报错截图,并来了句:数据能被修改,alert()也能被触发,但就是不触发watch事件,控制台报了这个错误。
我:。。。。。 哎呀 慌了啊。。这报错压根没遇过啊... 咋办咋办, 我的形象... 不.. 我的形象必须维护好,看了下报错信息大致是不能使用[0]的原因,稳妥为上,还是Google了一下报错,果然是不能使用[0],于是告知部门小妹原因。啊哈...感觉Google...
原本因为一切顺利的时候,部门小妹抛来了一句: 为什么不能使用[0]呢?....
此时此刻的我: ... 陷入了沉思... 我是谁 我要做什么,我要去哪
于是我便翻起了Vue的watch源码来看...(最痛苦的事,莫过于看源码了...,但也是收获最大的)
翻阅Vue源码
我们先来从Vue初始化的时候进行了什么操作
最先从initMixin(Vue)
看起
一看代码,还贼长,但是不用怕,我们只需要关注
initState(vm)
这个方法,我们的watch对象就是在这里面进行初始化的。我们再来看看
initState(vm)
这个方法对我们的watch都做什么初始化的操作
initState(vm)
从源码中可以看到,进行的初始化操作还挺多,分别有对我们的
props
、methods
、data
、computed
、watch
进行了初始化的操作,我们现在只关心watch的部分,所有只需要看initWatch(vm, opts.watch)
这个方法。
initWatch(vm, opts.watch)
参数:
- vm: 这里就是我的Vue
- watch:就是我们定义的watch对象
从源码可以看出,在这里把我们定义的每个watch都拿出来,最终丢进一个createWatcher()
方法里,而这里有两个createWatcher()
,从源码我们可以发现,原来我们的定义的watch可以是一个数组的,且元素也是一个watch。(get到一个新的watch使用方法)。
例如下面:
watch: {
a: [b, c],
b (val) {},
c (val) {}
}
最终initWatch会把我们定义的每个watch的key和value丢进createWatcher(vm, key, value)中,现在我们来看看createWatcher(vm, key, handler)
。
createWatcher(vm, key, handler)
通过源码可以看出,createWatcher的作用主要是取出我们定义的watch方法,然后传递给
vm.$watch(expOrFn, handler, options)
, 那么这里中的isPlainObject(handler)
是干嘛的呢? 别忘了Vue的watch有深度监听这个功能,当进行深度监听的时候,我们定义的就不说一个方法了,而是一个对象了,所以isPlainObject(handler)
if语句中主要是取出handler
这个方法。
watch: {
// 方法定义watch
a (value) { console.log(value)},
// 对象定义watch
b: {
handler (value) {
console.log(value)
},
immediate: false, // 是否一加载就执行
deep: true //深度遍历
}
}
接下来我们看看$watch
做了什么:
$watch
是在stateMixin(Vue)
中创建的:参数主要接受三个参数:
- expOrFn: 我们定义的watch中的key
- cb: 我们定义的watch中value,可以方法或者数组(上面也说过)
- opstions:就是当我们已对象定义watch的时候,那么options就指向这个对象
从源码中可以看出,$watch
主要就是调用两个方法:
- 又调用条件语句
isPlainObject(cb)
中的createWatcher(vm, expOrFn, cb, options)
,此处使用了递归重复调用,主要避免对象中的handler还是个对象,例如下面场景:
watch: {
a: {
handler: {
handler: {
handler (value) {
console.log(value)
}
}
}
}
}
- 最终都会调用
new Watcher(vm, expOrFn, cb, options)
最终返回一个unwatchFn()
方法,方法里调用了。
watcher.teardown()
方法,同上,该方法属于到Vue的响应式原理中的观察者模式,此篇文章暂且不进行详解,后面会出一片文章单独讲解。
下面我们来看看Watcher
Watcher
这个源码看着也不是很多,也才不带200行而已,咱们来解读解读:
先看看构造方法constructor
我们分三步来看:
- 第一步:主要就是对我们传过来的options对象的参数进行解析,是不是看到了属性的
deep
,你会发现除了常用的deep
外还有user
、lazy
、sync
、before
属性。 - 第二步:这一步的代码有木有发现 有木有发现有一段英文很熟悉?? 没错,就是部门妹子发给我的那个报错信息。这里就是我们这篇文章的中心背景了,部门妹子需要的答案就在这里的
parsePath()
中,把我们的定义的watch的key传入。让我们看看这个parsePath()
里面究竟是作什么的。
从源码中其实不难看出,该parsePath()
方法的作用主要是利用正则表达式进行判断我们定义的key是否符合watch的命名规范(只能存在.
的形式命名),然后split('.')进行拆分得到每个属性名key数组,这里就说明了只能使用.
形式的命名规则,因为源码就只使用了split('.')
进行拆分,所以部门小妹使用了'zndsList[0].isLoad' () {}
这个是无法解析的。然后利该parsePath()
方法利用闭包保存改key数组并返回一个方法(暂且称该方法为parse
),这个parse
方法作用是一步一步的取出value值,然后将其最终值value返回。这注意这个闭包方法parse
最终会赋值给Wather类的getter属性
上,谨记待会用得上。 - 第三步:第三步主要判断是否定义了
lazy:true
,当options
定义了lazy: true
, 那么当watch定义了immediate: true
的时候,刚初始化是不会去取Vue实例上data对象上的属性值的,而是直接返回一个undefined。当没有定义lazy
时,则调用get()
从Vue实例的data
对象中取对应的值返回给Wather
的value
属性。get()
的具体剖析在下面小节可以看到:
未定义lazy代码如下:
定义lazy代码如下:
当获取到this.value
赋值完之后,那么就应该回到我们的$watch
方法里了
箭头所指的代码就是立即调用我们定义的watch方法,并且把
value
传进去,这一步就是我们平时开发定义了immediate
让watch初始化便立即执行的原因所在。
现在我们回到Watcher类的get()
方法看看它都做了什么
第一行代码
pushTarget(this)
是把当前wather
实例对象push到观察者数组队列中去,该方法属于到Vue的响应式原理中的观察者模式,此篇文章暂且不进行详解,后面会出一片文章单独讲解。
我们主要看value = this.getter.call(vm, vm)
和if(this.deep) {traverse(value)}
这两部
value = this.getter.call(vm, vm)
上面有说到this.getter存的parsePath()
方法返回的闭包函数,其实它就长这样:
function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
通过这行代码value = this.getter.call(vm, vm)
直接执行了该闭包函数,返回了我们定义的watch方法的key对应的Vue实例上面的data的值,听起来是不是很绕口? 来来来...请看代码:
Vue({
data: {
obj: {
a: {
b: 'xiaoXX'
}
}
},
watch: {
'obj.a.b' (val) {
console.log(val)
}
}
})
parsePath()
方法返回的闭包函数相当于这样:
function (obj) {
var segments = ['obj', 'a', 'b']
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
所以value = this.getter.call(vm, vm)
执行完之后,value便是xiaoXX.
-
if(this.deep) {traverse(value)}
此处便是我们常用的深度遍历了,用官方的话就是:递归遍历一个对象来调用所有转换的getter,这样对象内嵌套的每个属性都作为“深度”依赖项被收集。这样对象的每个属性都会被监听到。
get()
到这里我们就知道该方法的作用是什么了,主要是调用this.getter()
获取我们定义的watch方法的key对应的Vue实例上面的data的值,(觉得还绕口的,赶紧再看看上面部门↑
.),在返回的时候顺便把判断是否启动了深度遍历模式deep: true
,若启动了就对对象的每个属性进行依赖收集。
最后每个Watcher类的实例对象都可以通过value进行获取data上对应的值。
回到我们的$watch
方法:
经过上面的分析,我们拿到了
watcher
实例,然后判断是否定义了options.immediate
,要是定义了立马执行我们定义的watch方法,我们平时常用的immediate: true
就是在这里使用的。
到这里我们的watch
从定义到执行的过程基本就解析完了,为什么叫基本呢? 因为还有一个watch
我们并没有说到,我们回顾下部门小妹的问题,定义的watch方法'zndsList[0].isLoad' () {}
之所以不能被执行,是因为源码执行了parsePath('zndsList[0].isLoad')
方法,因为这个方法是只接受简单的.
的格式。
但是你们在这个parsePath()
方法是在一个if-else里面,是可以跳过的
watch监听任意数据类型
请看源码:
- 第4部分
我们可以看出第4和第2是互斥的,我们定义的watch的key是一个方法function
时,就直接跳过了第2步,跳过了第2步就意味着不会进行key的解析,没有了parsePath('zndsList[0].isLoad')
的执行,那是不是意味着我们可以监听到'zndsList[0].isLoad'
呢? 不急,往下看:
我们上面说过get()
的是可以调用this.getter()
方法获取我们定义的watch方法的key对应的Vue实例上面的data的值,而parsePath()
就是返回一个能够获取我们data上面的数据的方法(记住返回的是方法,是方法,是方法)
,此时此刻聪明的你有没有想到什么?
没错,那么我们一开始定义的watch的key不是string
类型,而是function
类型的话不就可以跳过parsePath()
这一步了吗,对,是的,没错!但是属性又怎么能用function
当属性名呢? 对啊,当不了的,但是别忘了,Vue实例的定义的每一个watch是通过调用Vue.prototype.$watch
进行监听的,那么我们就可以直接调用原型上的$watch()
传递数据:
上代码:
-
点击修改之前
- 点击修改之后
嗯 妥妥的 完全可行...完美的避开了parsePath()
方法,通过上面原理可以发现,我们不单单可以监听数据类型的数据,只要是在data
上面的数据,我们都可以直接使用Vue.propertoty.$watch()
来实现监听。
思考总结,并引入Vue.prototype$watch的疑问
不禁思考其一个问题,既然可以使用Vue.propertoty.$watch()
来避开parsePath()
方法进行监听data上面的任意数据,那为什么parsePath()
这一步来限制呢? 不禁思考...陷入沉思...有哪位大佬知道吗,望告知...多谢!