背景
最近公司一位部门小妹妹@了我
小妹妹:我的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)
接下来我们看看`$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()
传递数据:
思考总结,并引入Vue.prototype$watch的疑问
不禁思考其一个问题,既然可以使用Vue.propertoty.$watch()
来避开parsePath()
方法进行监听data上面的任意数据,那为什么parsePath()
这一步来限制呢? 不禁思考…陷入沉思…有哪位大佬知道吗,望告知…多谢!
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: xiaoXX 原文链接:https://juejin.im/post/6866090889920872455