上一篇我们聊了提了3个问题,解答一个watch是如何收集依赖的,还剩下2个:
computed如果定义了但是没被引用?为什么就不会订阅Dep?
如果template只依赖了计算属性A,A又依赖了state里的a属性,很明显组件没有直接对于a的依赖,但是为什么修改了a组件也能更新呢?
和watch一样,计算属性的初始化也在 src/core/instance/state.js
中可以找到:
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]
let getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production') {
if (getter === undefined) {
warn(
`No getter function has been defined for computed property "${key}".`,
vm
)
getter = noop
}
}
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter, 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)
}
}
}
}
可以看到每个key对应的计算属性都新建了一个Watcher实例,并且最顶部有一个watcher option参数,lazy = true表示了这是个延迟计算的watcher。而在Watcher构造函数里(具体代码看上篇),lazy watcher有以下2句特殊的逻辑:
this.dirty = this.lazy // for lazy watchers
this.value = this.lazy ? undefined : this.get()
第一句直接对lazy watcher下了定义:你要是lazy,那你就是脏的,别人要用你,必须重新取值,也就是要调用watcher.evalute()方法,这样才能保证你是最新的:
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
第二句也就解释了为什么计算属性如果没被引用,就不会收集依赖。因为对于lazy watcher,第一次根本不会调用this.get()方法。
还剩下最后一个问题,试想一下这样的组件:
Vue.component('rectangle', {
data: function () {
return {
width: 0,
height: 0
};
},
computed: {
area() {
return this.width * this.height;
}
},
template: `
<span>area: {{ area }}</span>
`
});
width和height没有直接被template引用,那为什么修改他们组件也会更新?之前我们说过,组件的watcher是普通watcher,他会绑定组件的render方法,在第一次调用render的时候收集所以引用的依赖,那这种间接的依赖是怎么收集的呢?
我在没看计算属性代码前,第一直觉就是为每个计算属性也new一个Dep实例,然后拦截对应的计算属性函数,通知计算属性Dep对应的watcher。这样想想好像说得通,但是Vue不是这样实现的。
先说结论,对于上面这种情况,Vue会直接收集到computed的2个dep,然后订阅他们,下面我们就来看看Vue的奇技淫巧。
首先,我们得回头看一下Dep的pushTarget和popTarget方法:
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
这里有一个targetStack数组,或者说是栈,这个栈的作用就是当watcher.get执行过程中,如果遇到了别的watcher,就先把当前的watcher入栈,先执行别的watcher。上面说的间接依赖收集就借助了这个方法。
上面讲initComputed的时候,最后还有一个方法没讲,就是defineComputed,它的主要作用就是把computed属性代理到组件实例上,并且,拦截了计算属性的getter,setter:
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
? userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
我们就主要关注第一个if,它的get被设置为了一个createComputedGetter的返回值,set则被设置成了noop,也就是空方法(所以如果直接修改一个computed是没啥用的)。继续看看这个方法:
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
}
}
}
先看第一个if(watcher.dirty)
,表明这是一个没有及时更新的lazy watcher,或者简单的说就是计算属性的watcher, 就会调用evalute()来取值,在这个取值过程中,依赖就会被收集了。所以只要引用,计算属性就能订阅依赖的变化。
重点来了,第二个if(Dep.target)
里,调用了watcher的depend。我最初看的时候,无数次忽略了这个watcher.depend,总是习惯性的以为是dep.depend,然后想了一遍又一遍,一直没想到哪里可以收集到间接依赖。直到我反复的打断点,才突然发现,这TM是watcher.depend,不是dep.depend,那他是怎么实现的呢?
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
这个里面,this指向的是当前计算属性的watcher,而他的deps就是在dep.depend里被添加到watcher里的。
这里可能不太好理解,我们来梳理一下。
首先,1个Dep可以被多个Watcher订阅,但是1个Watcher也可以订阅多个依赖,比如我的面积属性,就依赖了长和宽。所以这样一来,Dep和Watcher其实是多对多的关系。
那知道一个Watcher所依赖的Dep有什么用呢?没错,就是依赖转移(我自己取得名字<_<),回到我们的问题。watcher.depend里遍历每个Dep,并且调用他们的depend方法,乍一看摸不着头脑,这不还是把计算属性的watcher添加到了dep里吗?
No No No!
再来看一下Dep.depend实现:
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
回想一下,上上一步,我们调用了watcher.evalute(),它的里面又调用了watcher.get(),get的最后又调用了popTarget,而popTarget返回的是出栈后栈顶的元素。而这个元素就是组件的watcher,所以一切迎刃而解了。通过遍历计算属性watcher的deps,让组件watcher去订阅他们。
妙,真的是妙啊!
弄懂了这个问题,我甚至兴奋得去楼下超市买了一袋干拌面!
不知道尤大写得时候花了多久想到这种方式,反正我光理解就花了很久,差距!