响应式原理-计算属性
计算属性的初始化是定义在 initState
函数中,执行了 if (opts.computed) initComputed(vm, opts.computed)
,initComputed
的定义在 src/core/instance/state.js
中:
function initComputed(vm: Component, computed: Object) {
// 注意这个 vm._computedWatchers 对象,后面的 createComputedGetter 方法中要用到了
// $flow-disable-line
const watchers = (vm._computedWatchers = Object.create(null));
// computed properties are just getters during SSR
const isSSR = isServerRendering();
for (const key in computed) {
const userDef = computed[key];
const getter = typeof userDef === "function" ? userDef : userDef.get;
if (process.env.NODE_ENV !== "production" && getter == null) {
warn(`Getter is missing for computed property "${key}".`, vm);
}
if (!isSSR) {
// create internal watcher for the computed property.
// 为每一个key创建Watcher实例
// 此时 watcher 的 getter 为 fullName () { return this.firstName + ' ' + this.lastName },但是不会立即求值,并且实例化一个 Dep
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
// 将每个key代理到 vm 上
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== "production") {
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
);
}
}
}
}
复制代码
initComputed
做了下面几件事:
- 定义
watchers
和vm._computedWatchers
- 遍历
computed
- 拿到用户自定义的
getter
,并为之创建computed watcher
- 如果
!key in vm
调用defineComputed
,利用Object.defineProperty
给计算属性的key
添加getter
和setter
defineComputed
定义在 src/core/instance/state.js
中:
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering();
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef;
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop;
sharedPropertyDefinition.set = userDef.set ? userDef.set : noop;
}
if (
process.env.NODE_ENV !== "production" &&
sharedPropertyDefinition.set === noop
) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码
createComputedGetter
定义在 src/core/instance/state.js
中:
function createComputedGetter(key) {
return function computedGetter() {
// this._computedWatchers 是在上面 initComputed 的时候添加到 vm 实例上的
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
watcher.depend();
// 调用 watcher.evaluate 就是求值,求 watcher 接收到的 expOrFn 的值
return watcher.evaluate();
}
};
}
复制代码
这就是 computed
的初始化做的事情,下面我们通过一个例子来看一下 computed
的获取和渲染过程。
<template>
<div id="app">
{{ fullName }}
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
firstName: "SHI",
lastName: "QI",
};
},
computed: {
fullName() {
return this.firstName + this.lastName;
},
},
methods: {
changeLast() {
this.lastName = "QILONG";
},
},
};
</script>
复制代码
当我们的 render
函数访问到 this.fullName
时,会触发计算属性的 getter
,即为:
function createComputedGetter(key) {
return function computedGetter() {
// this._computedWatchers 是在上面 initComputed 的时候添加到 vm 实例上的
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
watcher.depend();
// 调用 watcher.evaluate 就是求值,求 watcher 接收到的 expOrFn 的值
return watcher.evaluate();
}
};
}
复制代码
他会拿到计算属性的 watcher
,执行 watcher.depend
:
/**
* Depend on this watcher. Only for computed property watchers.
* 仅适用于 computed watchers
* 此时 Dep.target 是渲染 Watcher
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
复制代码
注意,此时 Dep.target
是渲染 Watcher
,所以 this.dep.depend()
相当于渲染 Watcher
订阅了这个 computed watcher
的变化。
然后执行 watcher.evaluate()
求值:
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
复制代码
evaluate
的逻辑非常简单,判断 this.dirty
,如果为 true
则通过 this.get()
求值,然后把 this.dirty
设置为 false
。在求值过程中,会执行 value = this.getter.call(vm, vm)
,这实际上就是执行了计算属性定义的 getter
函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName
。
在这个过程中,因为 firstName
和 lastName
都是响应式数据,所以会触发他们的 getter
,根据我们之前的分析,他会把自身持有的 dep
添加到目前正在计算的 watcher
中,这个时候 Dep.target
就是 computed watcher
。
最后通过 return this.value
返回最终值。我们目前知道了计算属性的求值过程,接下来看它依赖的数据变化之后的逻辑。
当我们修改 firstName
时,会触发他的 setter
方法,通知所有订阅它变化的 watcher
更新,执行 watcher.update()
方法:
/* istanbul ignore else */
if (this.computed) {
// 懒执行时走这里,比如 computed
// 将 dirty 置为 true,就可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true;
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify();
});
}
} else if (this.sync) {
// 在使用 $watch 或者 watch 选项时,可以传入一个 sync 选项,标识 watcher 需要同步更新
this.run();
} else {
// 一般的 watcher 更新都是异步队列,将 watcher 放入到更新对象队列
queueWatcher(this);
}
复制代码
对于计算属性,有两种模式 lazy
和 active
。如果 this.dep.subs.length === 0
成立,则说明没有人去订阅这个 computed watcher
的变化,仅仅把 dirty
设置为 true
,只有当再次访问的时候才会重新求值。在我们的场景下,渲染 watcher
订阅了这个 computed watcher
的变化,所以他会执行:
this.getAndInvoke(() => {
this.dep.notify()
})
getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
复制代码
getAndInvoke
会重新计算,对比新旧值(再次计算的时候,因为之前已经添加过依赖,所以在 watcher.depIds
中已经缓存)。如果新旧值不一致则执行回调函数,那么这里这个回调函数是 this.dep.notify()
,在我们这个场景下就是触发了渲染 watcher
重新渲染。
总结
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher
,也了解了它的创建过程和被访问触发 getter
以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue
想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher
重新渲染,本质上是一种优化。
参考:Vue.js 技术揭秘