回忆
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 中:
traversetraverse 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 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,可以根据不同的场景做相应的配置。
网友评论