计算属性
计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
侦听器
watch 响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
计算属性 VS 侦听器
计算属性是计算出一个新的属性,并将这个属性挂载到 vm(Vue 实例上)。侦听器是监听 vm 上已经存在的响应式属性,所以可以用侦听器监听计算属性。
计算属性本质是一个惰性求值的观察者,具有缓存性,只有当依赖发生改成时,才会重新求值。侦听器是当依赖数据发生改变时就会执行回调。
在使用场景上,计算属性适合在一个数据被多少数据影响时使用,而侦听器适合在一个数据影响多个数据。
Vue2 Computed 原理分析
Vue 2.6.11
在 Vue2 中进行实例初始化时,会进行很多初始化,包括:初始化生命周期、初始化事件、初始化injections、初始化state(props,methods,data,computed,watch) 等等 。当在初始化 state 时就会进行 computed 的初始化。涉及到函数就是 initState。
function initMixin (Vue) {Vue.prototype._init = function (options) {var vm = this;...// 初始化props,methods,data,computed,watchinitState(vm);...};}
调用 initState 函数会进行数据状态的初始化,在 Vue 中 props、methods、data、computed、watch 都可以被称为状态,所以被统一到 initState 函数中进行初始化。但是这里需要注意是先初始化 data,在初始化 computed,最后在初始化 watch。这个顺序其实是有一定讲究的。计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。本质上计算属性是依赖响应式属性的,所以需要先将响应式属性初始化。而侦听器是监听 vm 上已经存在的响应式属性,实质上也是可以用侦听器监听计算属性的,所以 watch 是在计算属性初始化完之后进行初始化。
function initState (vm) {...// 初始化数据if (opts.data) {initData(vm);} else {observe(vm._data = {}, true /* asRootData */);}// 初始化计算属性if (opts.computed) { initComputed(vm, opts.computed); }// 初始化监听if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch);}...}
接着调用 initComputed 函数进行 computed 的初始化,这里有几个点需要了解一下。
- 获取计算属性的定义 userDef 和 getter 求值函数,在 Vue 中定义一个计算属性有两种方法,一种是直接写一个函数,另外一种是添加 set 和 get 方法的对象形式。
- 计算属性的观察者 watcher 和 消息订阅器 dep。watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历 dep.subs 通知每个 watcher 更新。在创建 watcher 时传递了四个参数:vm 实例、getter 函数、noop 空格函数(watcher 的回调)、computedWatcherOptions 常量{ lazy: true }
在进行 Watcher 实例化时,传入常量{ lazy: true },会给当前 watcher 打上两个标记,一个标记是 lazy = true 表示当前 watcher 是计算属性的 watcher,一个标记是 dirty = ture,用于后续求值时标记是否需要重新求值 。
function initComputed (vm, computed) {// $flow-disable-linevar watchers = vm._computedWatchers = Object.create(null);// computed properties are just getters during SSRvar isSSR = isServerRendering();// 遍历 computed 对象,为每一个属性进行依赖收集for (var key in computed) {// 1.var userDef = computed[key];// 获取 getvar getter = typeof userDef === 'function' ? userDef : userDef.get;if (!isSSR) {// 2.watchers[key] = new Watcher(vm, // vm 实例getter || noop, // getter 求值函数或者是一个空函数noop, // 空函数 function noop(a, b, c) {}computedWatcherOptions // computedWatcherOptions 常量对象 { lazy: true };);}if (!(key in vm)) {// 3.defineComputed(vm, key, userDef);} else {if (key in vm.$data) {warn(("The computed property "" + key + "" is already defined in data."), vm);} else if (vm.$options.props && key in vm.$options.props) {warn(("The computed property "" + key + "" is already defined as a prop."), vm);}}}}
因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm 实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。
defineComputed 定义计算属性。
if (!(key in vm)) {defineComputed(vm, key, userDef);} else {...}function defineComputed (target,key,userDef) {...Object.defineProperty(target, key, sharedPropertyDefinition);}
在 defineComputed 最后调用了原生的 Object.defineProperty 方法,并且在 Object.defineProperty(target, key, sharedPropertyDefinition); 传入属性描述符 sharedPropertyDefinition。 描述符初始化值为:
var sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop};
在 defineComputed 时,根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinition 的 get/set 方法在经过 userDef 和 shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition 的 get 函数也就是createComputedGetter(key) 的结果。
var shouldCache = !isServerRendering();if (typeof userDef === 'function') {sharedPropertyDefinition.get = shouldCache? createComputedGetter(key): createGetterInvoker(userDef);sharedPropertyDefinition.set = noop;} else {sharedPropertyDefinition.get = userDef.get? shouldCache && userDef.cache !== false? createComputedGetter(key): createGetterInvoker(userDef.get): noop;sharedPropertyDefinition.set = userDef.set || noop;}if (sharedPropertyDefinition.set === noop) {sharedPropertyDefinition.set = function () {warn(("Computed property "" + key + "" was assigned to but it has no setter."),this);};}
我们找到 createComputedGetter 函数调用结果并最终改写 sharedPropertyDefinition 大致呈现如下:
sharedPropertyDefinition = {enumerable: true,configurable: true,get: function computedGetter () {var watcher = this._computedWatchers && this._computedWatchers[key];if (watcher) {if (watcher.dirty) {watcher.evaluate();}if (Dep.target) {watcher.depend();}return watcher.value}},set: userDef.set || noop}
当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher。执行方法 evaluate。这个方法只有懒惰的观察者才会这样做。
Watcher.prototype.evaluate = function evaluate () {this.value = this.get();this.dirty = false;};
到这里计算属性的初始化就已经完成,那计算属性又是如何根据响应式进行依赖缓存的了?
其实我们不难发现,当 vue 在执行 evaluate 方法时,本质上还是通过 watcher.get 来获取结算结果,当计算属性依赖的数据发生变化时,就会触发 set 方法,通知更新触发 update 方法。这是会将标记 dirty 设置为 ture,当再次调用computed 的时候就会重新计算返回新的值。
Watcher.prototype.update = function update () {// computed Watcherif (this.lazy) {this.dirty = true;} else if (this.sync) { // watch Watcherthis.run();} else {queueWatcher(this);}};
Vue3 Computed 原理分析
Vue 3.2.36
为了防止一部分同学对 vue3 的 computed 不是很熟悉,这里也会简单说下使用方式。
第一种使用方式,接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。
const count = ref(1)const plusOne = computed(() => count.value + 1)console.log(plusOne.value) // 2plusOne.value++ // 错误
第二种使用方式,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
const count = ref(1)const plusOne = computed({get: () => count.value + 1,set: val => {count.value = val - 1}})plusOne.value = 1console.log(count.value) // 0
从使用方式来看,其实 vue3 和 vue2 机会是没有差别的。都是基于响应式依赖进行缓存。
举个例子:
const data = { name: '张三', age: 18 };const state = reactive(data);const newAge = computed(() => state.age + 1);effect(() => {document.getElementById('app').innerHTML = `${state.name}, 今年刚刚好${newAge.value}岁`});
vue3 computed依赖reactive/ ref 响应属性的值进行计算,而 effect 依赖 computed 的值进行计算。
- computed 是 effect
- 变量 newAge 通过 age 计算而来
- 变量 age 收集了 computedEffect,而对于 computed 来说它收集渲染 effect。

computed 本身有两种使用方式:const xxx = computed(() => xxx)const xxx1 = computed({get: () => {}, set: () => {}})
当我们在调用 computed 方法时,就会在这里需要统一做下区分,同时调用实现类ComputedRefImpl,这个方法比较简单,接下来我们重点分析下类ComputedRefImpl。
function computed(getterOrOptions, ...) {let getter;let setter;const onlyGetter = isFunction(getterOrOptions);if (onlyGetter) {getter = getterOrOptions;setter = () => {console.warn('Write operation failed: computed value is readonly');};}else {getter = getterOrOptions.get;setter = getterOrOptions.set;}const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR);...return cRef;}
ComputedRefImpl 类 :
class ComputedRefImpl {constructor(getter, _setter, isReadonly, isSSR) {this._setter = _setter;this.dep = undefined;this.__v_isRef = true;this._dirty = true;// 1.this.effect = new ReactiveEffect(getter, () => {if (!this._dirty) {this._dirty = true;triggerRefValue(this);}});this.effect.computed = this;this.effect.active = this._cacheable = !isSSR;// 根据传入是否有setter函数来决定是否只读this["__v_isReadonly" /* IS_READONLY */] = isReadonly;}get value() {const self = toRaw(this);trackRefValue(self);if (self._dirty || !self._cacheable) {self._dirty = false;self._value = self.effect.run();}return self._value;}set value(newValue) {this._setter(newValue);}}
这里将 ComputedRefImpl 类分为两块来解读。
第一块就是 **computed** 的初始化,调用 ComputedRefImplconstructor 初始化,主要做两件事情:
- 创建 effect 对象,生成 watcher 监听函数并赋值给实例的 effect 属性。将当前 getter 当做监听函数,并附加调度器。
- 设置 computed ref 是否只是可读。设置是否可读的依据是:
onlyGetter||!setter
不过单单从构造方法来看其实和 computed 没有太大的关系,只是进行了初始化变量的操作,并创建了一个 ComputedRef 实例赋值给我们的调用。
我们发现声明一个 computed 时其实并不会执行 getter 方法,只有在读取 computed 值时才会执行它的 getter 方法,那么接下来我们就要关注 ComputedRefImpl 的 getter 方法。
上面提到的,第二部分就是 getter 方法的执行,getter 方法会在读取 computed 值的时候执行,而在 getter 方法中有一个叫 _dirty 的变量,它的意思是代表脏数据的开关,默认初始化时 _dirty 被设为 true ,在 getter 方法中表示开关打开,需要计算一遍 computed 的值,然后关闭开关,之后再获取 computed 的值时由于 _dirty 是 false 就不会重新计算。这就是 computed 缓存值的实现原理。
get value() {...if (self._dirty || !self._cacheable) {self._dirty = false;self._value = self.effect.run();}return self._value;}
那么 computed 是怎么知道要重新计算值的呢?
computed 本身是依赖响应式属性的变化的,如果依赖的响应属性发生改变,会触发 effect 的 scheduler 函数执行。此方法就是 computed 内部依赖的状态变化时会执行的操作。所以最终的流程就是:computed 内部依赖的状态发生改变,执行对应的监听函数,这其中自然会执行 scheduler 里的操作。而在 scheduler 中将 _dirty 设为了 true 。
this.effect = new ReactiveEffect(getter, () => {// effect 的 scheduler 函数执行if (!this._dirty) {this._dirty = true;triggerRefValue(this);}});
也许看到这里有同学还会产生一个疑问,computed 是怎么知道内部依赖产生了变化呢?这是由于在我们第一次获取 computed 值(即执行getter方法)的时候对内部依赖进行了访问,在那个时候就对其进行了依赖收集操作,所以 computed 能够知道内部依赖产生了变化。
注意:上面提到的「第一次获取 computed 值」这里是第一次或者,而不是初始化 computed。
调试 Computed
Vue 3.2 +
在 Vue 3.2 + 的版本中,新增了 computed 调试的功能,computed 可接受一个带有 onTrack 和 onTrigger 选项的对象作为第二个参数:
onTrack 和 onTrigger 仅在开发模式下生效。
- onTrack 会在某个响应式 property 或 ref 作为依赖被追踪时调用。
- onTrigger 会在侦听回调被某个依赖的修改触发时调用。
这个调试 computed 在源码实现也比较简单,在 computed 初始化的时候,会将这个两个方法挂载 effect 上。const plusOne = computed(() => count.value + 1, {onTrack(e) {// 当 count.value 作为依赖被追踪时触发debugger},onTrigger(e) {// 当 count.value 被修改时触发debugger}})// 访问 plusOne,应该触发 onTrackconsole.log(plusOne.value)// 修改 count.value,应该触发 onTriggercount.value++
当 computed 的 getter 被执行时,会触发跟踪依赖属性的function computed(getterOrOptions, debugOptions, isSSR = false) {...const cRef = new ComputedRefImpl(...);if (debugOptions && !isSSR) {cRef.effect.onTrack = debugOptions.onTrack;cRef.effect.onTrigger = debugOptions.onTrigger;}return cRef;}
trackRefValue方法,如果存在 onTrack 就会执行 onTrack 回调。 ```javascript class ComputedRefImpl { … get value() { … trackRefValue(self); … } }
function trackRefValue(ref) { … trackEffects(ref.dep || (ref.dep = createDep()), { target: ref, type: “get” / GET /, key: ‘value’ }); … }
function trackEffects(dep, debuggerEventExtraInfo) { … activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo)); … } ``` 类似的当依赖的属性被修改时,会触发 onTrigger 方法。
总结
不管在是 Vue 2 还是在 Vue 3 中,对 computed 本身的实现原理基本都是一样的。当使用 computed 计算属性时,组件初始化会对每一个计算属性都创建对应的 watcher , 然后在第一次调用自己的 getter 方法时,收集计算属性依赖的所有 data,那么所依赖的 data 会收集这个订阅者同时会针对 computed 中的 key 添加属性描述符创建了独有的 get 方法,当调用计算属性的时候,这个 get 判断 dirty 是否为 true,为真则表示要要重新计算,反之直接返回 value。当依赖的 data 变化的时候回触发数据的 set 方法调用 update() 通知更新,此时会把 dirty 设置成 true,所以 computed 就会重新计算这个值,从而达到动态计算的目的。
