分享一次computed原理结合案例

253 阅读4分钟

第一次尝试写技术文章,结合之前看过的一些讲解computed的文章,以及结合案例我将分享我自己的理解,如有错误还请指出,虚心请教。

实例:computed里的b值依赖于data里面a的值

<template>
    <div>
        <button @click="getB">获取b的值</button>
    </div>
</template>
<script>
export default {
    data(){
        return{
            a:1
        }
    },
    computed:{
        b(){
            return this.a+4
        },
        c:{
        	get(){
            	return this.a+2
            }
        }
    },
    mounted(){
    	console.log(this.b)
        this.initGetB()
    },
    methods:{
    	initGetB(){
        	console.log(this.b)
        },
        getB(){
            this.a=9
            console.log(this.b)
        }  
    }
}
</script>

先看一下计算属性的初始化发生在Vue实例初始阶段的initState函数中,根据if(opts.computed)执行了initComputed(vm, opts.computed)

看一下initComputed函数

const computedWatcherOptions = { lazy: true }

function initComputed(vm: Component, computed: Object) {
  // 首先创建了watcher,vm._computedWatchers为一个空对象
  const watchers = vm._computedWatchers = Object.create(null)
  // 判断是否是服务端渲染
  const isSSR = isServerRendering()
  //遍历computed对象
  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
      )
    }

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
    //这里也解释了为什么computed里面有的属性data里面无需定义
      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对象遍历const userDef = computed[key]相对案例即:const userDef=conputed['b']=function(){return this.a+4}。判断userDef是函数还是对象,因为computed还有一种以对象的形式通过set,get来设值即案例中c值的写法,无论怎样都是获取对应的getter方法。
  • 为computed对应的每一个key创建一个computed watcher
  • 在确定计算属性与已有data、props属性不重名的情况下,调用defineComputed

继续看一下defineComputed的实现

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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
  }
  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)
}

defineComputed核心就是调用Object.defineProperty(target,key,sharedPropertyDefinition)将computed这个key挂载到vm上,当访问这个属性的时候(this.b实际上就是访问vm上的b属性而不是computed里面的b属性)就会调用getter,在上面我们定义了sharedPropertyDefinition这个对象针对对象的key监听做的处理

再看一下createComputedGetter这个函数

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
    }
  }
}

其中watcher.evaluate根据dirty来决定是computed独有的函数。 这个方法返回了computedGetter函数,他就是计算属性对应的getter

最后看一下watcher类

// /src/core/observer/watcher.js
export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.dirty = this.lazy // for lazy watchers
    
      this.value = this.lazy
      ? undefined
      : this.get()
  }
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    //执行了计算属性定义的getter函数
    value = this.getter.call(vm, vm)
    popTarget()
    this.cleanupDeps()
    //通过return value拿到计算属性对应的值
    return value
  }
 
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }
  
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

首先this.dirty=this.lazy这个根据传入的lazy初始化的时候设置为true 下面就根据实例的调用来分析一下过程

1.在mounted里面调用console.log(this.b)时,会针对b这个属性执行对应的get函数,也就是之前通过sharedPropertyDefinition.get = createComputedGetter(key)(我们不考虑是不是服务器渲染这种情况)调用createComputedGetter函数,createComputedGetter函数根据dirty初始化为true时,执行watcher里面的evaluate函数。evaluate函数又根据this.dirty=true去调用this.get()函数,执行value=this.getter.call(vm,vm),实际上就是执行了计算属性定义的getter函数,返回value值。

2.在mounted里接着执行this.initGetB()函数时,由于之前console.log(this.b)执行时将dirty设置了false,所以就不再执行evaluate调用get函数了,而是直接将上一次通过this.get()函数获取并设置在this上的value值返回。这就是computed缓存的点

3.当点击按钮button执行getB的时候,改变了a的值,而b依赖a,分析一下这个依赖的过程。 当我们修改a值的时候,就会触发a属性对应的setter,在a属性对应的setter里面会通知所有订阅它变化的watcher,也就是去执行watcher.update函数,在update函数里根据lazy重新将dirty设置为了true,方便下次再调用的时候重新调用get函数而不是直接获取之前的value值

以上就是个人见解,最关键的就是根据dirty值来控制是否来取缓存还是重新计算。