来自一位部门小妹妹的灵魂拷问:Vue的watch怎么没有效果的?

源自:https://juejin.im/post/6866090889920872455

背景

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

小妹妹:我的watch监听不到属性,是哪里写错了吗? 但是明明感觉写的没错呀?

心想:Vue的watch又不是什么难的东西,嗯,来机会了,可以让我树立一个完美的形象了,哈哈哈哈...

装作淡定的回道: 你把代码截图发我看看

看了代码之后。。。。。 瞬间就不淡定了, 我看不出有啥问题啊


image

代码如下:


image

监听数组的第一个元素的对象中某个属性不就是这样写的吗'zndsList[0].isLoad' () {}应该不会错的,应该是部门小妹修改数据的事件没有触发,对,肯定是了。

于是我淡定的回了句:你在修改的方法里面alert()看看有没有触发修改的方法,这应该是没有触发值的修改所以才没有触发watch的(心想,嗯,搞定,so easy...)。

谁知部门小妹发来了张报错截图,并来了句:数据能被修改,alert()也能被触发,但就是不触发watch事件,控制台报了这个错误。


image

image

我:。。。。。 哎呀 慌了啊。。这报错压根没遇过啊... 咋办咋办, 我的形象... 不.. 我的形象必须维护好,看了下报错信息大致是不能使用[0]的原因,稳妥为上,还是Google了一下报错,果然是不能使用[0],于是告知部门小妹原因。啊哈...感觉Google...

原本因为一切顺利的时候,部门小妹抛来了一句: 为什么不能使用[0]呢?....

此时此刻的我: ... 陷入了沉思... 我是谁 我要做什么,我要去哪

于是我便翻起了Vue的watch源码来看...(最痛苦的事,莫过于看源码了...,但也是收获最大的)

翻阅Vue源码

我们先来从Vue初始化的时候进行了什么操作

最先从initMixin(Vue)看起

image

一看代码,还贼长,但是不用怕,我们只需要关注initState(vm)这个方法,我们的watch对象就是在这里面进行初始化的。
我们再来看看initState(vm)这个方法对我们的watch都做什么初始化的操作

initState(vm)

image

从源码中可以看到,进行的初始化操作还挺多,分别有对我们的propsmethodsdatacomputedwatch进行了初始化的操作,我们现在只关心watch的部分,所有只需要看initWatch(vm, opts.watch)这个方法。

initWatch(vm, opts.watch)

image

参数:

  • 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)

image

通过源码可以看出,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做了什么:

image

$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

image

这个源码看着也不是很多,也才不带200行而已,咱们来解读解读:

先看看构造方法constructor

image

我们分三步来看:

  • 第一步:主要就是对我们传过来的options对象的参数进行解析,是不是看到了属性的deep,你会发现除了常用的deep外还有userlazysyncbefore属性。
  • 第二步:这一步的代码有木有发现 有木有发现有一段英文很熟悉?? 没错,就是部门妹子发给我的那个报错信息。这里就是我们这篇文章的中心背景了,部门妹子需要的答案就在这里的parsePath()中,把我们的定义的watch的key传入。让我们看看这个parsePath()里面究竟是作什么的。
    image

    从源码中其实不难看出,该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对象中取对应的值返回给Wathervalue属性。get()的具体剖析在下面小节可以看到:

未定义lazy代码如下:


image

定义lazy代码如下:


image

当获取到this.value赋值完之后,那么就应该回到我们的$watch方法里了

image

箭头所指的代码就是立即调用我们定义的watch方法,并且把value传进去,这一步就是我们平时开发定义了immediate让watch初始化便立即执行的原因所在。

现在我们回到Watcher类的get()方法看看它都做了什么

image

image

第一行代码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方法:

image

经过上面的分析,我们拿到了watcher实例,然后判断是否定义了options.immediate,要是定义了立马执行我们定义的watch方法,我们平时常用的immediate: true就是在这里使用的。

到这里我们的watch从定义到执行的过程基本就解析完了,为什么叫基本呢? 因为还有一个watch我们并没有说到,我们回顾下部门小妹的问题,定义的watch方法'zndsList[0].isLoad' () {}之所以不能被执行,是因为源码执行了parsePath('zndsList[0].isLoad') 方法,因为这个方法是只接受简单的.的格式。

但是你们在这个parsePath()方法是在一个if-else里面,是可以跳过的

watch监听任意数据类型

请看源码:


image
  • 第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()传递数据:

image

image

上代码:


image
  • 点击修改之前


    image
  • 点击修改之后
    image

    嗯 妥妥的 完全可行...完美的避开了parsePath()方法,通过上面原理可以发现,我们不单单可以监听数据类型的数据,只要是在data上面的数据,我们都可以直接使用Vue.propertoty.$watch()来实现监听。

思考总结,并引入Vue.prototype$watch的疑问

不禁思考其一个问题,既然可以使用Vue.propertoty.$watch()来避开parsePath()方法进行监听data上面的任意数据,那为什么parsePath()这一步来限制呢? 不禁思考...陷入沉思...有哪位大佬知道吗,望告知...多谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352