建议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:表示计算属性的gettercomputedWatcherOptions:一个配置对象{ 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,所以又会对计算属性重新求值。