1 介绍
版本:2.5.17
我们使用vue-vli创建基于Runtime+Compiler的vue脚手架。
学习文档:https://ustbhuangyi.github.io/vue-analysis/reactive/computed-watcher.html
2 功能
我们知道computed实时检测属性有没有发生变化,当发生变化时,相应computed的属性也会随之发生变化。
<script>
export default {
name: 'App',
render(createElement) {
return createElement('div',{
attrs: {
id:'app1'
}
},this.message)
},
data() {
return {
message: 'hello vue',
a: 1,
b: 2,
}
},
computed: {
c: function () {
return this.a + this.c;
}
}
}
</script>
3 源码分析
3.1 initState
我们从之前的一章节(vue的源码学习之五——2.数据驱动(new Vue发生了什么)) 知道, Vue 实例初始化阶段,运行initState函数。
其定义在src\core\instance\state.js中:
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 */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
执行了 if (opts.computed) initComputed(vm, opts.computed)。
3.2 initComputed
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)
}
}
}
}
- 函数创建
vm._computedWatchers
为一个空对象。 - 对
computed
对象做遍历,拿到计算属性的每一个userDef
- 尝试获取这个
userDef
对应的getter
函数,拿不到则在开发环境下报警告。 - 为每一个
getter
创建一个watcher
,这个watcher
和渲染watcher
有一点很大的不同,它是一个computed watcher
,因为const computedWatcherOptions = { computed: true }
。 - 对判断如果
key
不是vm
的属性,则调用defineComputed(vm, key, userDef)
,否则判断计算属性对于的key
是否已经被data
或者prop
所占用,如果是的话则在开发环境报相应的警告。
3.3 defineComputed
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.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 通常是计算属性是一个对象,并且拥有 set
方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key)
的返回值。
3.4 createComputedGetter
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}
createComputedGetter
返回一个函数 computedGetter
,它就是计算属性对应的 getter。
4 computed watcher 与 通的 watcher的区别
4.1 例子分析
我们知道计算属性是一个 computed watcher
,它和普通的 watcher
有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher
的实现。
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
当初始化这个 computed watcher
实例的时候,构造函数部分逻辑稍有不同:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
可以发现 computed watcher
会并不会立刻求值,同时持有一个 dep
实例。
4.2 watcher.depend
当我们的 render
函数执行访问到 this.fullName
的时候,就触发了计算属性的 getter
,它会拿到计算属性对应的 watcher
,然后执行 watcher.depend()
,来看一下它的定义:
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意,这时候的 Dep.target
是渲染 watcher
,所以 this.dep.depend()
相当于渲染 watcher
订阅了这个 computed watcher
的变化。然后再执行 watcher.evaluate()
去求值
4.3 evaluate
evaluate()
的定义:
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
evaluate
的逻辑非常简单:
- 判断
this.dirty
,如果为true
则通过this.get()
求值 - 把
this.dirty
设置为 false。 - 在求值过程中,会执行
value = this.getter.call(vm, vm)
,这实际上就是执行了计算属性定义的getter
函数, - 在我们这个例子就是执行了
return this.firstName + ' ' + this.lastName
。
这里需要特别注意的是,由于 this.firstName
和 this.lastName
都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep
添加到当前正在计算的 watcher
中,这个时候 Dep.target
就是这个 computed watcher
。
最后通过 return this.value
拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。
4.4 update
一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher
更新,执行 watcher.update()
方法:
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
那么对于计算属性这样的 computed watcher
,它实际上是有 2 种模式,lazy 和 active。如果 this.dep.subs.length === 0
成立,则说明没有人去订阅这个 computed watcher
的变化,仅仅把 this.dirty = true
,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher
订阅了这个 computed watcher
的变化,那么它会执行:
this.getAndInvoke(() => {
this.dep.notify()
})
getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
getAndInvoke
函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify()
,在我们这个场景下就是触发了渲染 watcher
重新渲染。
5 总结
我们知道计算属性本质上就是一个 computed watcher
,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher
重新渲染,本质上是一种优化。