我们知道计算属性的结果是会被缓存的,除非它依赖的响应式 property 变化才会重新计算。比如我们下面的fullName属性,只有它依赖的surName或者name发生变化,才会重新计算。 例1:
computed: {
fullName() {
return this.surName + this.name
}
}
那么它的这个缓存功能究竟怎么实现的呢?今天我们就来探究一下
首先是computd的初始化过程:
computed的初始化发生在data初始化之后,通过initComputed函数完成
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
/* computed初始化看这里 */
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initComputed函数会遍历我们传入的computed配置项,并把每个computed创建成一个个计算属性watcher,同时添加到vm实例的_computedWatchers属性上。
注意new Watcher时传入的computedWatcherOptions配置项,它是实现缓存的重要标志
watcher创建工作完成后,它通过defineComputed函数把我们配置项里的每一个computed都添加到vm实例上,同时定义了它们的存取描述符,即给每个属性都添加了getter、setter用于数据劫持
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
/* 通过Object.defineProperty 把计算属性定义到vm实例上, 并对访问和设置进行劫持 */
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
当我们访问计算属性时(vm.fullName) 会触发computedGetter函数, computedGetter函数根据watcher.dirty判断当前数据是否是脏数据(dirty为true表明是脏数据 需要重新计算)如果是脏数据则执行evaluate方法, evaluate方法只做两件事,一个是执行watcher的get方法获取value,一个是把dirty属性重置为false,以表明当前数据不是脏数据
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
}
}
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
下面是watcher实现的具体过程
let uid = 0
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
if (options) {
this.lazy = !!options.lazy
}
this.cb = cb
this.id = ++uid // uid for batching
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.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
}
这里我同样只保存了和缓存相关的代码,我们在创建wather时传入的options里有{laze: true}选项,所以我们的计算属性watcher默认lazy和dirty为ture,所以在我们在new Watcher时不会直接调用get方法,这和渲染wather不同。 这也是为什么当我们的computed刚初始化完成时,这个计算属性watcher的value为undefined。
当我们第一次去访问这个属性时(vm.fullName)会触发computedGetter方法,由于计算属性的dirty默认为true,所以会执行watcher.evaluate()方法,evaluate方法会调用get求值,得到value,同时置dirty为false。
当第二次去访问这个属性时,同样会触发computedGetter,但由于此时dirty为false,所以不会 执行evaluate,而是直接返回watcher.value 这就是使用了缓存
当我们的计算属性watcher所依赖的property发生变化后首先会被响应式系统劫持,然后派发更新,所谓派发更新其实就是依次调用所有依赖了这个属性的wather的update方法,update方法首先会判断你的lazy是不是为true,如果为true的话,把dirty属性置为true。那么当我们再去访问vm.fullName属性时就又会像我们第一次访问属性时一样执行evaluate方法。