来自一位部门小妹妹的灵魂拷问:Vue的watch怎么没有效果的?__Vue.js
发布于 4 年前 作者 banyungong 2159 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

背景

最近公司一位部门小妹妹@了我

小妹妹:我的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)

从源码中可以看到,进行的初始化操作还挺多,分别有对我们的propsmethodsdatacomputedwatch进行了初始化的操作,我们现在只关心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`这个方法。 ```JavaScript 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()这一步来限制呢? 不禁思考…陷入沉思…有哪位大佬知道吗,望告知…多谢!

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: xiaoXX 原文链接:https://juejin.im/post/6866090889920872455

回到顶部