源码中计算属性的实现
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true })
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}}
vm参数就是当前组件的实例,computed参数就是我们写的技术属性对象,
isServerRendering()方法是获取当前的运行环境是否是服务器渲染环境,计算属性在服务器渲染环境下是失效的,从const getter = typeof userDef === 'function' ? userDef : userDef.get可以得出,我们写计算属性时有两者写法
//写法一
computed: {
num(){
return 1 + 1
}
}
//写法二
computed:{
num: {
get(){
return 1 + 1
}
}
}
接下来就是new了一个观察者来挂载计算属性的回调函数,然后再把这个观察者挂载到实例属性上的_computedWatchers属里面,我们知道计算属性是一个惰性求职,只有当你在使用的时候他才会进行求职,因此给观察者的配置项里面传递了lazy = true ,最后是一个defineComputed方法,我们看看defineComputed方法干了什么
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
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
这里看着挺复杂,其实最重要的是 Object.defineProperty(target, key, sharedPropertyDefinition),将计算属性代理到组件的实例上,我们知道,我们在配置项data里面写的属性,后面都可以通过this.xxx来获取到,而计算属性这里也是,我们写了个计算1+1的计算方法num,然后在组件实例上console.log(this.num)会得出2
总结
计算属性的实现方式就一个惰性的观察者,然后在通过 Object.defineProperty劫持到组件实例上,然后就可以直接通过this.xxx来访问
计算属性缓存功能的实现
我们知道计算属性有缓存功能,无论我们在视图中引用了多少次变量,它只会进行一次求值,二次就不会了,除非计算属性里面的所依赖的变量发生了变化才会重新进行求职,例如
<div id="app" @click="eventClick">
{{ num }} + {{ num }}
</div>
new Vue({
el: '#app',
computed: {
num(){
console.log(1); //只会打印一次1
return 1
}
})
它是怎么实现缓存的呢?其实很简单,当首次引用num时,watcher会调用num的回调函数获取值1,然后把1保存到watcher里面的value字段上,当二次调用num时它就直接返回value字段的值,而这个控制求值的字段就是watcher上面dirty字段,这个字段默认是false, 当当前观察者的lazy属性是true时它就为dirty,看下面代码
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
key就是我们传入的num,这里获取到num计算属性的观察者,然后判断dirty属性为true后进行调用watcher的evaluate()方法,此方法会调用num计算属性的回调方法获得返回值,然后将dirty设置为false,后续二次获取num值都是直接return watcher.value
计算属性的更新机制
<div id="app" @click="eventClick">
{{ num }} + {{ num }}
</div>
new Vue({
el: '#app',
data:{
count: 1
},
computed: {
num(){
return count
}
})
我们知道,num的回调函数的值是count也就是当count变化时就会进行重新求值,然后触发视图的更新,那么num计算属性是如何知道count是否变化了呢?count并没有在视图界面上使用
那就要从Vue响应式的响应式说起了,看下面代码
<div id="app" @click="eventClick">
{{ age }}</div>
new Vue({
el: '#app',
data:{
age: 1
}
})
Vue的设计是一个组件一个观察者,当前id='app'的视图也算是一个组件,它是叫“根组件”,因为age属性在根组件上被引用了,那么age属性就会收集到当前根组件的观察者,当它自身的值变化了就会去通知根组件观察者去更新视图,
回到上面的问题,因为count属性没有在页面中使用,只在num计算属性这个观察者中使用了,因此当count变化时它会通知num计算属性观察者去重新求得最新的值,却无法更新视图,因为视图观察者并没有收集到count属性,那怎么办呢?
答案就是:让count属性也主动收集一下视图观察者,无论count属性是否在不在页面中使用
看下面代码
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
* if (Dep.target) {
* watcher.depend()
* }
* return watcher.value
}
}
看星号标注的代码,当num计算属性使用到了count值,那么count就会收集到num计算属性这个观察者,然后num观察者调用depend方法通知count属性去收集一下Dep.target所指向的观察者,此时我们只需要把Dep.target指向视图观察者,那么就可以在count变化时,它会触发num观察者重新求得最新的值,求完值后然后通知视图观察者去更新视图