Vue计算属性的实现和更新机制

350 阅读4分钟

源码中计算属性的实现

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观察者重新求得最新的值,求完值后然后通知视图观察者去更新视图