系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
在Vue中可以使用计算属性,缓存中间的计算结果,只有在相关响应式依赖发生变化时,它们才会重新求值,从而避免重复的计算,提高性能,那么接下来,就来看看计算属性在Vue中是如何实现的。
computed
在初始化Vue实例的过程中,会调用initState方法处理数据,在该方法中,如果检测到有computed选项,就会调用initComputed方法,处理计算属性,代码如下所示:
/* core/instance/state.js */
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
// ...
if (opts.computed) initComputed(vm, opts.computed)
// ...
}
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
// $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.
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)) {
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方法中,首先定义一个空对象_computedWatchers,用来存放计算属性相关的Watcher实例;然后遍历computed选项,对计算属性进行处理。
因为计算属性支持单个函数,也支持带get、set属性的对象,所以首先取得计算属性的取值函数getter,然后在非服务端渲染的情况下,创建计算属性对应的Watcher实例,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
// ...
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// ...
}
this.value = this.lazy
? undefined
: this.get()
}
}
可以看到,在创建Watcher实例的过程中,这里的getter就是上面计算属性的getter,而options就是computedWatcherOptions,它的lazy选项为true,所以Watcher实例的lazy和dirty属性为true,由于lazy为true,所以对于计算Watcher来说,在创建时不会调用get方法进行求值。
回到上面的initComputed方法中,在创建好Watcher后,将其添加到_computedWatchers中。接着判断计算属性是否已经存在于当前Vue实例上,如果不存在,就调用defineComputed方法,将其添加到当前Vue实例上,代码如下所示:
/* 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)
: 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 (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)
}
可以看到,在defineComputed方法中,给当前Vue实例上添加新的访问器属性,其属性名是计算属性的属性名,而在非服务端渲染的情况下,它的get访问器是通过调用createComputedGetter方法创建的,其代码如下所示:
/* 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,而这个新函数就是对应计算属性的get访问器,所以当我们访问该计算属性时,就会执行computedGetter方法,那么接下来,就来看看在访问计算属性时,Vue又做了哪些工作。
computedGetter
当访问计算属性时,会触发计算属性的get访问器,也就是computedGetter方法。在该方法中,首先会从_computedWatchers中取出对应的Watcher实例,然后通过watcher.dirty属性判断该计算属性是否需要重新计算,首次访问时,dirty属性为true,所以会调用evaluate方法,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
evaluate() {
this.value = this.get()
this.dirty = false
}
}
可以看到,在evaluate方法中,首先会调用watcher.get方法,在调用pushTarget方法后,会将Dep.target指向当前的计算Watcher,然后执行getter函数,也就是我们在配置选项中编写的函数,如果此计算属性依赖于data选项中的数据,那么就会触发该数据的get访问器,所以当前的计算Watcher会将该数据添加到自己的依赖中,同时此数据对应的dep也会将计算Watcher添加到它的观察者列表中,接下来的popTarget和cleanupDeps方法的逻辑就和之前相同,最终,将watcher.get方法返回的值赋值给watcher.value,然后将dirty置为false,表示当前计算属性已经是最新值,不用重新计算。
在执行完evaluate方法后,此时计算属性与它所依赖的数据之间就产生了联系,修改所依赖的数据时,就会通知计算属性进行更新。但是在computedGetter方法中,除了调用evaluate方法外,还有一段逻辑,如果此时Dep.target存在,就会调用watcher.depend方法,再进行一次依赖收集,可以想到这样的一个场景,在组件的渲染过程中,Dep.target首先会指向渲染Watcher,当访问计算属性时,会将渲染Watcher推入targetStack栈中,调用完getter方法后,又会将Dep.target指回渲染Watcher,由于此时Dep.target存在,所以就会执行watcher.depend方法,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
/* core/observer/dep.js */
export default class Dep {
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
可以看到,在depend方法中,这里的this指向的还是计算Watcher,this.deps中保存的是计算属性依赖的数据,当执行dep.depend方法时,这里的Dep.target指向的却是渲染Watcher,所以又会在渲染Watcher和数据之间构建联系。
最终,在访问计算属性时,计算Watcher和渲染Watcher都会收集对此数据的依赖,所以当此数据发生变化时,会同时通知计算Watcher和渲染Watcher进行更新操作。那么接下来,就来看看计算Watcher是如何进行更新的。
update
当依赖的数据发生变化时,就会遍历观察者集合,调用它们的update方法,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
可以看到,对于计算Watcher来说,由于它的lazy选项为true,所以它只会将dirty属性置为true,表示该计算属性需要重新进行计算,但不会将计算Watcher推入watcher queue中。这么做的好处是,如果更新后的渲染Watcher不依赖于该计算属性,就不用执行计算属性的重新求值了,只有当下次又访问到该计算属性时,由于在之前已经将dirty属性为true,所以才会执行计算属性的重新计算。
总结
在Vue中,可以通过计算属性对计算的结果进行缓存,避免重复计算的开销。在计算Watcher收集依赖的过程中,会将依赖的数据同步代理到上级Watcher中,所以在数据发生变化时,才会执行组件的重新渲染。