computed 和 watch
很多时候我们使用 computed 时往往会与 Vue 中另一个 API 也就是侦听器 watch 相比较,因为在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
从 Vue 官方文档对 watch 的解释我们可以了解到,使用 watch 选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。
下面还另外总结了几点关于 computed 和 watch 的差异:
- computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm 上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 data、props)
- computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而 watch 则是当数据发生变化便会调用执行函数
- 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据
原理分析
Vue 响应系统,其核心有三点:observe、watcher、dep:
- observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;
- dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;
- watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。
Vue 之模板渲染过程中 new Vue 第 3 点[初始化 computed,wacther]。
initState
1 | export function initState(vm: Component) { |
initComputed
1 | const computedWatcherOptions = { computed: true }; |
Watcher(已移除部分代码
1 | class Watcher { |
Dep(已移除部分代码
1 | export default class Dep { |
defineComputed
1 | export function defineComputed( |
看完上面的源码,总结下来就是:
当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observer 遍历 data 中每个属性设置 get/set 数据拦截
初始化 computed 会调用 initComputed 函数:
注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher )
调用计算属性时会触发其 Object.defineProperty 的 get 访问器函数
调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。
当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者 wathcer 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。
[越努力,越幸运!]