计算属性 VS 侦听属性
Vue 的组件对象支持了计算属性 和侦听属性 2 个选项,很多同学不了解什么时候该用 什么时候该用 。先不回答这个问题,我们接下来从源码实现的角度来分析它们两者有什么区别。
计算属性的初始化是发生在 Vue 实例初始化阶段的 函数中,执行了 , 的定义在 中:
函数首先创建 为一个空对象,接着对 对象做遍历,拿到计算属性的每一个 ,然后尝试获取这个 对应的 函数,拿不到则在开发环境下报警告。接下来为每一个 创建一个 ,这个 和渲染 有一点很大的不同,它是一个 ,因为 。 和普通 的差别我稍后会介绍。最后对判断如果 不是 的属性,则调用 ,否则判断计算属性对于的 是否已经被 或者 所占用,如果是的话则在开发环境报相应的警告。
那么接下来需要重点关注 的实现:
这段逻辑很简单,其实就是利用 给计算属性对应的 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 的返回值,来看一下它的定义:
返回一个函数 ,它就是计算属性对应的 getter。
整个计算属性的初始化过程到此结束,我们知道计算属性是一个 ,它和普通的 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 的实现。
当初始化这个 实例的时候,构造函数部分逻辑稍有不同:
可以发现 会并不会立刻求值,同时持有一个 实例。
然后当我们的 函数执行访问到 的时候,就触发了计算属性的 ,它会拿到计算属性对应的 ,然后执行 ,来看一下它的定义:
注意,这时候的 是渲染 ,所以 相当于渲染 订阅了这个 的变化。
然后再执行 去求值,来看一下它的定义:
的逻辑非常简单,判断 ,如果为 则通过 求值,然后把 设置为 false。在求值过程中,会执行 ,这实际上就是执行了计算属性定义的 函数,在我们这个例子就是执行了 。
这里需要特别注意的是,由于 和 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 添加到当前正在计算的 中,这个时候 就是这个 。
最后通过 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。
一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 更新,执行 方法:
那么对于计算属性这样的 ,它实际上是有 2 种模式,lazy 和 active。如果 成立,则说明没有人去订阅这个 的变化,仅仅把 ,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 订阅了这个 的变化,那么它会执行:
函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 ,在我们这个场景下就是触发了渲染 重新渲染。
通过以上的分析,我们知道计算属性本质上就是一个 ,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 重新渲染,本质上是一种优化。
接下来我们来分析一下侦听属性 是怎么实现的。
watch
侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 函数中,在 初始化之后,执行了:
来看一下 的实现,它的定义在 中:
这里就是对 对象做遍历,拿到每一个 ,因为 Vue 是支持 的同一个 对应多个 ,所以如果 是一个数组,则遍历这个数组,调用 方法,否则直接调用 :
这里的逻辑也很简单,首先对 的类型做判断,拿到它最终的回调函数,最后调用 函数, 是 Vue 原型上的方法,它是在执行 的时候定义的:
也就是说,侦听属性 最终会调用 方法,这个方法首先判断 如果是一个对象,则调用 方法,这是因为 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 实例化了一个 ,这里需要注意一点这是一个 ,因为 。通过实例化 的方式,一旦我们 的数据发送变化,它最终会执行 的 方法,执行回调函数 ,并且如果我们设置了 为 true,则直接会执行回调函数 。最后返回了一个 方法,它会调用 方法去移除这个 。
所以本质上侦听属性也是基于 实现的,它是一个 。其实 支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。
Watcher options
的构造函数对 做的了处理,代码如下:
所以 总共有 4 种类型,我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。
deep watcher
通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:
这个时候是不会 log 任何数据的,因为我们是 watch 了 对象,只触发了 的 getter,并没有触发 的 getter,所以并没有订阅它的变化,导致我们对 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。
而我们只需要对代码做稍稍修改,就可以观测到这个变化了
这样就创建了一个 了,在 执行 求值的过程中有一段逻辑:
在对 watch 的表达式或者函数求值后,会调用 函数,它的定义在 中:
的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 ,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 记录到 ,避免以后重复访问。
那么在执行了 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 的回调函数了。
对 的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 为 true,但是因为设置了 后会执行 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。
user watcher
前面我们分析过,通过 创建的 是一个 ,其实它的功能很简单,在对 求值以及在执行回调函数的时候,会处理一下错误,如下:
在 Vue 中是一个错误捕获并且暴露给用户的一个利器。
computed watcher
几乎就是为计算属性量身定制的,我们刚才已经对它做了详细的分析,这里不再赘述了。
sync watcher
在我们之前对 的分析过程知道,当响应式数据发送变化后,触发了 ,只是把这个 推送到一个队列中,在 后才会真正执行 的回调函数。而一旦我们设置了 ,就可以在当前 中同步执行 的回调函数。
只有当我们需要 watch 的值的变化到执行 的回调函数是一个同步过程的时候才会去设置该属性为 true。
总结
通过这一小节的分析我们对计算属性和侦听属性的实现有了深入的了解,计算属性本质上是 ,而侦听属性本质上是 。就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。
同时我们又了解了 的 4 个 ,通常我们会在创建 的时候配置 和 ,可以根据不同的场景做相应的配置。
领取专属 10元无门槛券
私享最新 技术干货