建议PC端观看,移动端代码高亮错乱
关于
computed
在@2.5
之前的旧版本中的实现可以参考黄轶老师的文章
计算属性的初始化是发生在 Vue
实例初始化阶段的 initState
函数中:
export function initState (vm: Component) {
// ...
if (opts.computed) initComputed(vm, opts.computed)
// ...
}
initComputed
的定义在 src/core/instance/state.js
中:
1. 初始化过程
// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
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.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 对于组件来说,在创建子组件构造函数时已经调用了 defineComputed,并将 computed 定义在其原型上
// 只有对于当根实例来说,才会执行此处的 defineComputed
if (!(key in 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)
}
}
}
}
- 创建
vm._computedWatchers
为一个空对象,用来保存computed watcher
。 - 对
computed
对象做遍历,拿到计算属性的getter
。 - 为每一个
getter
创建一个computed watcher
。 - 判断如果
key
不是vm
的属性,则调用defineComputed(vm, key, userDef)
。
1.1 实例化computed watcher
实例化时传入构造函数的 4 个参数:
// src/core/instance/state.js
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
getter
:表示计算属性的getter
computedWatcherOptions
:一个配置对象{ lazy: true }
,表示这是一个computed watcher
看下构造函数的逻辑稍有不同:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
this.value = this.lazy
? undefined
: this.get()
}
和渲染 watcher
不一样的是,由于我们传入配置的 lazy
是 true
,所以不会立刻调用 this.get()
进行求值
1.2 defineComputed
注意这里 Vue
有一个优化处理,在创建组件构造函数时:
// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
// ...
if (Sub.options.computed) {
initComputed(Sub)
}
// ...
}
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
这里提前调用了 defineComputed
,并且第一个参数传入的是组件的原型,也就是 Comp.prototype
。
这样做的目的就是避免多次实例化同一组件时,在实例上重复调用 defineComputed
方法。
下面来看看 defineComputed
函数的定义:
// src/core/instance/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
if (typeof userDef === 'function') {
// 简化后的
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
} else {
// 简化后的
sharedPropertyDefinition.get = userDef.get
? createComputedGetter(key)
: noop
sharedPropertyDefinition.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)
}
这段逻辑很简单,其实就是利用 Object.defineProperty
给计算属性对应的 key
值添加 getter
和 setter
。
在平时的开发场景中,计算属性有 setter
的情况比较少,我们重点关注一下 getter
部分,最终 getter
对应的是 createComputedGetter(key)
的返回值,来看一下它的定义:
// src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
createComputedGetter
返回一个函数 computedGetter
,它就是计算属性对应的 getter
。
整个计算属性的初始化过程到此结束,下面结合例子和过程来分析。
2. 过程分析
有如下例子:
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
2.1 依赖收集
当我们的 render
函数执行访问到 this.fullName
的时候,就触发了计算属性的 getter
,也就是在 createComputedGetter
中返回的 computedGetter
:
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
首先拿到这个计算属性的 computed watcher
。
这里会对 watcher.dirty
进行判断,dirty
是用来标志是否已经执行过计算结果,这是因为只有在相关响应式数据发生变化时,computed
才会重新求值,其余情况多次访问计算属性的值都会返回之前计算的结果,这就是缓存的优化。
2.1.1 watcher.evaluate
当首次执行时,dirty
为 false
,因此调用 watcher.evaluate
进行求值,evaluate
函数如下:
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
- 执行
this.get()
进行求值。 - 将
dirty
置为false
,当下次访问computed
时,可以直接取watcher.value
,达到缓存目的。
在执行 this.get()
进行求值的过程中会执行 value = this.getter.call(vm, vm)
,这实际上就是执行了用户定义的计算属性的 getter
函数,在我们这个例子就是执行了:
return this.firstName + ' ' + this.lastName
由于 this.firstName
和 this.lastName
都是响应式对象,这里会触发它们的 getter
,根据我们之前的分析,它们会把自身持有的 dep
添加到当前正在计算的 watcher
中,这个时候 Dep.target
就是这个 computed watcher
,同时也将 computed watcher
添加到了 dep
的 subs
队列中。
2.1.2 watcher.depend
回到计算属性的 getter
中接着往下执行
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
这时会执行 watcher.depend
进行依赖的收集:
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
此时 deps
保存的是 this.firstName
和 this.lastName
对应的 dep
。然后调用他们的 depend
方法。由于当前 Dep.target
恢复成 渲染 watcher
了,所以就构造了 渲染 watcher
和 dep
之间的关系。
最后通过 return watcher.value
拿到计算属性对应的值。
2.2 派发更新
派发更新的条件是 computed
中依赖的数据发生改变,在本例中就是 this.firstName
或 this.lastName
发生改变。下面来看看如果 this.firstName
发生改变时发生了什么。
- 会调用
this.firstName
的setter
,进而执行dep.notify
。 - 当执行
dep.notify
方法时,会遍历subs
数组,然后依次调用sub.update
。在本例中this.firstName
的dep.subs
数组如下[computedWatcher, 渲染watcher]
。

2.2.1 computed watcher 执行 update
当执行 computed watcher
的 update
方法时:
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
因为对于 computed
来说,lazy
返回,所以 update
过程不会执行状态更新的操作,只会将 dirty
标记为 true
。
2.2.2 渲染 watcher 执行 update
当执行 渲染 watcher
的 update
方法时:
会执行 updateComponent
进行视图重新渲染,而 render
过程中会访问到计算属性,此时由于 this.dirty
值为 true
,所以又会对计算属性重新求值。