Vue 计算属性原理

869 阅读3分钟

计算属性原理

从源码看计算属性。分两块来讲解计算属性初始化流程计算属性watcher的触发流程

计算属性是定义在vm(vue实例)上的一个特殊的getter方法,他的getter并不是用户提供的函数,而是vue内部设置的代理函数。

计算属性初始化流程

beforcreatecreate之间。

initComputed 初始化所有的计算属性

const computeWatcherOptions = { lazy : true }
function initComputed(vm,computed){
    const watchers = vm._computedWatchers = Object.create(null)
    
    const isSSR = isServerRending()
    
    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("...")
        }
        
        if(isSSR){
            watchers[key] = new Watcher(
              vm,
              getter || noop,
              noop,
              computeWatcherOptions
            )
        }
        
        if(!(key in vm)){
            defineComputed(vm , key , userDef)
        }else if(process.env.NODE_ENV !== 'production'){
            if(key in vm.$data){
                warn('...')
            }else if(vm.$options.props && n vm.$options.props){
                warn('...')
            }
        }
    }
}

好了,我们一步一步来说。先说两点noop是一个空函数function(){}这样,warn是报错函数。vm就是当前的Vue实例computed就是当前实例上的所有计算属性。

const computeWatcherOptions = { lazy : true }
//computeWatcherOptions申明当前的Watcher是一个计算属性的Watcher
const wathcer = vm._computedWatchers = Object.create(null)
//会在vm实例上创建_computedWatchers,用来保存计算属性的Watcher依赖

开始遍历computed的所有计算属性,开始初始化。

//开始遍历computed的所有计算属性,开始初始化。
for(const key in computed){
        const userDef = computed[key]
        //userDef获取当前的计算属性的值
        const getter = typeof userDef === 'function' ? userDef : userDef.get 
        //计算属性可以是函数也可以是对象,对象需要定义get和set属性。
        
        if(process.env.NODE_ENV !== 'production' && getter == null){
            warn("...")
        }
        //当不是开发环境并且getter为null时报错
        ...
}

例
computed:{
    b : function(){
        return this.a + 1
    },
    c : {
        get :function (){
            return this.a + 1
        },
        set :function (){
            return this.a + 1
        }
    }
}

key 为 b时,userDef为function(){return this.a + 1},getter因为b是函数所以也是function(){return this.a + 1}
key 为 c时,userDef为{get:function(){...},set:function(){...}},getter因为c是对象所以是get:function(){...}
        if(isSSR){
            watchers[key] = new Watcher(
              vm,
              getter || noop,
              noop,
              computeWatcherOptions
            )
        }
        服务端的话就不会生成计算属性的依赖,服务端也不会缓存数据,也不会通知。就是一个普通的getter,每次获取都要重新计算一遍。
        生成Watcher时会将用户设置的函数传入依赖保存。后面可以用于重新计算数据。
        if(!(key in vm)){
            defineComputed(vm , key , userDef)
        }else if(process.env.NODE_ENV !== 'production'){
            if(key in vm.$data){
                warn('...')
            }else if(vm.$options.props && n vm.$options.props){
                warn('...')
            }
        }
        判断计算属性的key值是否已经在vm上了,如果存在data和props之上则报错计算属性失效。如果存在methods之上虽然不会报错,但是计算属性会默默失效。
        不存在vm上的话开始对这个计算数据进入defineComputed

defineComputed把计算属性修改为存储描述符

懒得写源码了,这块直接说吧

const sharePropertyDefiniton = {
    enumentable: true,
    configurable: true,
    get: noop,
    set: noop
}

export function defineComputed(target,key,userDef){
    ...
    处理get和set
    Object.defineProperty(target,key,sharePropertyDefiniton)
}

主要就是处理get和set的流程

函数

服务端

sharePropertyDefiniton.get = userDef || noot

sharePropertyDefiniton.set = noop || noot

客户端

sharePropertyDefiniton.get = createPropertyGetter(key) || noot

sharePropertyDefiniton.set = noop || noot

对象

服务端

sharePropertyDefiniton.get = userDef.get || noot

sharePropertyDefiniton.set = userDef.set || noot

客户端

sharePropertyDefiniton.get = createPropertyGetter(key) || noot

sharePropertyDefiniton.set = userDef.set || noot

最后如果set是noot的话set = function (){warn("...")},一个报错函数。然后设置Object.defineProperty。

createPropertyGetter就是要说的第二部分,上面是计算属性的创建流程。createPropertyGetter就是触发依赖流程。

createPropertyGetter触发依赖流程

这块比较抽象,createPropertyGetter是一个闭包函数。了解了这个就能了解计算属性的缓存结果和依赖触发机制。

function createPropertyGetter(key){
    return PropertyGetter(){
        const watcher = this._computedWatchers && this._computedWatchers[key]
        
        if(watcher){
            if(watcher.dirty){
                watcher.evaluate()
            }
            if(Dep.target){
                watcher.depend()
            }
            return watcher.value
        }
    }
}

好了,这里我们也分两块理解

缓存值

dirty为true时说明计算属性所用到的数据发生了变化,需要重新计算。

class watcher {
...
evaluate : function(){
    this.value = this.get()
    this.dirty = false
}
...
}
重新计算一遍,保存值。再把dirty修改为false

依赖

if(Dep.target){
    watcher.depend()
}
            
class watcher {
...
depend : function(){
let i = this.deps.length
while(i--){
    this.deps[i].depend()
}
}
...
}


computed:{
    b : function(){
        return this.a + 1
    }
}

我们看上面的例子。

  • 计算属性b如果被模板使用,b依赖于a
  • a的依赖的dep中会有b的watcher和模板(组件watcher)
  • 当a发生变化会通知b把dirty修改为true,同时通知模板(组件watcher)开始渲染流程
  • 模板读取计算属性b,重新计算b的值用于渲染

我们来说说a是什么时候收集到

计算属性的wathcer

当计算值时会触发a的get,这时候a就收集了计算属性b的依赖了。

模板的wathcer

调用计算属性b时,b中有一个deps保存了所有依赖数据的watcher。b的deps中就有[a的wathcer]。触发所有依赖的depend,就可在a中收集模板的依赖。

if(Dep.target){
    watcher.depend()
}

这就是它的作用

问题

这个模式也有一个问题,计算结果哪怕与上次相同也还是会是模板重新进入渲染流程。所有在2.5.17版本后修改了defineComputed。惊不惊喜,开不开心。

function createPropertyGetter(key){
    return PropertyGetter(){
        const watcher = this._computedWatchers && this._computedWatchers[key]
        
        if(watcher){
            watcher.depend()
            return watcher.evalute()
        }
    }
}

watcher中
evalute:function(){
    if(this.ditry){
        this.value=this.get()
        this.dirty=false
    }
    return this.value
}
depend:function(){
    if(this.dep && Dep.target){
      this.dep.depend()
    }
}
  • 模板使用计算属性,计算属性将组建watcher保存到dep.subs
  • 当数据发生变化时通知计算属性
  • 先触发计算属性中的update,这里判断dep.subs中是否有依赖
  • 没有依赖将dirty改为true
  • 有依赖重新计算值,如果不同调用dep.subs中所有的依赖通知他们计算属性发生了变化