1、关于computed的一些问题
computed属性是什么时候初始化的?- 为什么不能在
data中使用computed? - 怎样设置computed不缓存结果, 就算依赖未发生变化也重新计算?
- 假设计算属性b依赖组件数据data.a, 那么当data.a改变时是怎么通知到使用了计算属性b的watcher的?
2、computed的初始化
computed的初始化是在this._init里完成的。
this._init里面调用了initState方法。从下面的代码可知data是在computed之前进行初始化的,所以是访问不到computed属性。那如果执意要访问呢?
initState内部调用initComputed对computed初始化。
3、initComputedcom
initComputed的大概逻辑是:
- 遍历我们定义好的
computed对象,也就是options中的computed对象。每一个属性都创建一个对应的Watcher对象。Watcher对象的get方法就是我们定义的computed属性的getter。因为computedWatcherOptions设置了lazy为true,所以这里getter不会立即执行。 defineComputed(vm, key, userDef)在组件实例上定义一个与computed的key同名的属性, 这就是为什么我们能直接通过this访问。
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// ....
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
// ...
defineComputed(vm, key, userDef)
}
}
4、defineComputed
computed属性是通过Object.defineProperty(target, key, sharedPropertyDefinition)定义的访问器属性。- 访问器属性的set方法就是用户定义
computed时定义的set方法,如果没有定义则是一个noop函数。 - 访问器属性的get方法处理逻辑要复杂些。
- 如果定义的
computed属性只是一个函数- 在浏览器时是
createComputedGetter(key)创建get方法 - 服务端是
createGetterInvoker(userDef)创建get方法
- 在浏览器时是
- 如果定义的
computed属性只是一个对象- 当定义
computed时指定了cache: false, 使用createGetterInvoker(userDef)创建get方法。 - 未指定
cache: false, 使用createComputedGetter(key)创建get方法。
- 当定义
- 如果定义的
// userDef.cache可以控制是否缓存computed属性的值
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
)
}
}
// 组件实例访问key其实访问的是对应watcher的value
Object.defineProperty(target, key, sharedPropertyDefinition)
}
5、createComputedGetter
createComputedGetter创建的get会使用缓存。
- 在第3节我们已经知道了每个computed属性都会有一个对应的watcher,当第一访问computed属性时,
watcher.dirty的值是true。因为我们创建Watcher的时候传入的lazy为true。
所以会通过watcher.evaluate(),调用get计算computed的值, 计算得到的结果是缓存在watcher.value上的。
第一次计算完后watcher.dirty的值变成false,所以只要watcher.dirty不变成true,watcher.evaluate()就不会重复调用。
- 那么
watcher.dirty的值什么时候会重新变成true呢?
当调用
watcher.update的时候就会更新dirty的值为true。熟悉Vue响应式原理的朋友应该知道,当Observer对象的发生改变时就会通过Dep对象去执行Watcher的update方法。所以当computed属性的依赖发生变化时,只是更改了dirty的值, 只有当下一次访问computed属性执行get方法时才会重新计算。
computed本质上也是一个watcher对象,那么它是怎样被其它watcher对象订阅的呢?
上面的代码会遍历computedWathcer订阅的数据,使Dep.target也重新订阅一边。
// 会对computed属性进行缓存
function createComputedGetter (key) {
return function computedGetter () {
// 访问computed属性时,其实访问的时内部watcher的值
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果Watcher的值需要重新计算时
// 在evaluate内部会执行用户定义的getter, 在getter内依赖的数据都会被watcher收集
// computed手机依赖
if (watcher.dirty) {
watcher.evaluate()
}
// 如果Dep.target存在的话, 可能是renderWathcer 也可能是computedWatcher、userWatcher
// 会将watcher收集的依赖同时被Dep.target收集
// 举个例子: computed属性a依赖 $data.b
// 在template中使用了a, 那么renderWatcher也会间接依赖$data.b
// 当$data.b的值变化时,会将watcher.dirty的值变成true
// 同时也会通知renderWatcher进行patch
if (Dep.target) {
// 将computed收集到的依赖给使用了这个computed属性的Watcher也弄一份
// 所以当computed属性的依赖发生变化时,computedWatcher的dirty值变成true, 但式不会立马重新求值
// 同时也会通知到使用了这个computed属性的watcher(比如renderWatcher),在执行renderWatcher时使用到这个computed属性时才会重新求值。
watcher.depend()
}
// 返回watcher的值
return watcher.value
}
}
}
7、createGetterInvoker
不使用watcher进行结果缓存。
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}