计算属性vs侦听属性(二)

回忆

        watch的过程就是订阅数据,数据更新时执行回调函数。关于渲染,渲染Watcher本身就订阅了数据变化,userWatcher又会比渲染Watcher先执行。

        watch侦听属性的初始化也是发生在Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行initWatche(vm, opts.watch),故事就开始了...

       因为watch的同一个key可以对应多个handler。所以遍历每个watck(key),执行createWatcher()。

       createWatcher(vm,key,handler),因为watch下属性可以是函数或对象。1、a(){...}或者2、a:{deep: true, immediate: true, handler}。所以去取handler函数会先判断,如果是1,拿到的是一个函数直接执行vm.$watch(expOrFn(字符串'a'),handler,opt)。

        vm.$watch(expOrFn,handler,opt),1、因为可以直接调用vm.$watch,所以第一步会通过createWatcher去进行数据规范化。2、定义一个user Watcher,new Watcher(vm, expOrFn, cb, options)。3、如果有设置immediate为true,这里会先把handler执行一次。4、teardown 方法去移除这个user watcher

        new Watcher(vm, expOrFn, cb, options)。如果为createWatcher第一种情况,expOrFn为'a'字符串,cb是回调函数。this.getter=parsePath(expOrFn)(parsePath功能是把expOrFn处理成一个由‘.’分割的数组,这里即['a'],最后返回一个函数赋值给getter(在之后get()函数把dep.Target改为当前userWatcher后再执行,那样订阅该数据的为userWatcher),函数功能是遍历上面取得的数组,进行访问(比如this.a),访问触发依赖收集在该userWatcher中订阅该数据该数据的sub实例中会加入该userWatcher)。如果第二种情况,watch的如果是comput中的属性,不同的地方时求this.getter时,会触发creatComputedGetter方法,返回函数computedGetter(功能是watcher.depend,return watcher.evaluate()。这里的watcher是computedWatcher),也就是该uW会订阅该cW(cW的this.sub中会加入该uW,当cW依赖的响应式属性变化通知cW,cW通知uW)

        到这里,我们对侦听属性的初始化监听过程介绍完了,下面介绍当侦听数据发生变化时

        deep会在watcher.get()执行,递归遍历watch的属性值给当前Watcher订阅这些属性,不然深层属性发生变化没Watcher去通知。 immediate会在$.watch执行(第这样一次渲染时候就会执行一次回调函数,不用等到第一次改变)。

接下来我们来分析一下侦听属性 watch 是怎么实现的

        watch监听的响应式属性发生改变触发userWatcher.run()。watcher.run()会执行watcher.get()去取新值,和旧值做比较,如果发生变化就执行回调函数(即handler)。watch监听的computed属性发生改变也同理。

开始

        watch(),侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了:

侦听属性的初始化

来看一下 initWatch 的实现,它的定义在 src/core/instance/state.js 中:

initWatch

        这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher:

createWatcher

        这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:

stateMixin

        也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher。  

        所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。

Watcher options

Watcher 的构造函数对 options 做的了处理,代码如下:

所以 watcher 总共有 4 种类型

我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。

deep watcher

        通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:

深度观测

        这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象所以也并不会触发 watch 的回调函数了(没有渲染Watcher订阅该响应式属性)

而我们只需要对代码做稍稍修改,就可以观测到这个变化了

这样就创建了一个 deep watcher 了 

在 watcher 执行 get 求值的过程中有一段逻辑:

在对 watch 的表达式或者函数求值后,会调用 traverse 函数

它的定义在 src/core/observer/traverse.js 中:

traverse

        traverse 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。

        那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。

        对 deep watcher 的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。

user watcher

        前面我们分析过,通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,如下:

handleError 在 Vue 中是一个错误捕获并且暴露给用户的一个利器。

computed watcher

        computed watcher 几乎就是为计算属性量身定制的,我们刚才已经对它做了详细的分析,这里不再赘述了。

sync watcher

        在我们之前对 setter 的分析过程知道,当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。

        只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过程的时候才会去设置该属性为 true

总结

        通过这一小节的分析我们对计算属性和侦听属性的实现有了深入的了解,计算属性本质上是 computed watcher,而侦听属性本质上是 user watcher。就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

        同时我们又了解了 watcher 的 4 个 options,通常我们会在创建 user watcher 的时候配置 deep 和 sync,可以根据不同的场景做相应的配置。        

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

推荐阅读更多精彩内容