【Vue2.x原理剖析二】计算属性原理

422 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

前言

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大

计算属性的初始化

计算属性可以写成一个函数形式,也可以写成对象形式,但对象形式必须要有get方法

// /src/core/instance/state.js
function initComputed (vm: Component, computed: Object) {
  // 定义缓存watcher的值
  const watchers = vm._computedWatchers = Object.create(null)
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 判断key是否在vm上定义过
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
    ...
    }
    if (!isSSR) {
      // 将每个属性初始化watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,//回调函数noop()=>{}
       computedWatcherOptions// 配置,初始值为{lazy:true}表示为计算属性
      )
    }
  }
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 判断是不是ssr
  const shouldCache = !isServerRendering()
  //当计算属性为方法时,定义get方法
  if (typeof userDef === 'function') {
    // 重写get方法
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 获取对象时的get方法
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  
  // 将计算属性放到vm上并对计算属性的get和set做劫持
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 重写计算属性的get方法,判断是否需要重新计算
function createComputedGetter (key) {
  return function computedGetter () {
    // 获取相应计算属性key定义的watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 如果是脏的,需要重新求值,初始值时true,初次要经过一次计算
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 如果dep还存在target,这时候一般为渲染watcher,计算属性依赖的数据也需要收集
      // 在对象形式时,这一步非常重要,在重新计算完值后,全局的栈中还有一个渲染watcher,得靠他去更新视图
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

watcher中计算属性相关

重点两个变量lazy:表示是否为计算属性,计算属性初始化时默认值为truedirty:表示是否为脏数据,如果为脏数据,需要重新计算,计算属性初始化时默认值为true

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    vm._watchers.push(this) //为了能强制更新
    // options
    if (options) {
      this.user = !!options.user
      this.lazy = !!options.lazy// 标识计算属性watcher
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.active = true
    this.dirty = this.lazy // 表示watcher是否需要重新计算,默认为true
    this.deps = []
    this.newDeps = []
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    // 非计算属性实例化会默认调用get方法进行取值,计算属性的实例化时候不会去调用get
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 计算属性在这里执行用户定义的get函数,访问计算属性依赖项 从而把自身计算watcher添加到依赖项dep里面收集起来
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps() //不清理 可能上次的数据还要被再次收集 vm.a = [1,2,3] => vm.a = {}数组不需要在收集
    }
    return value
  }

  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  update () {
    // 计算属性的依赖值发生了变化,只需要把dirty设置为true,下次访问到了就重新计算
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 异步队列机制
      queueWatcher(this)
    }
  }

  run () {
    if (this.active) {
      // 为当前watcher设置最新值,此处的value和oldValue是为watch功能做工作
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  // 获取值,并把dirty设置为false,表示已经计算过值
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  depend () {
    // 计算属性的watcher储存了依赖项的dep
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()//调用依赖项的dep去收集watcher,将当前watcher储存起来
    }
  }
}

通过案例掌握计算属性执行逻辑

理一理计算属性的执行逻辑,分两种情况:

  • 函数形式
<body>
  <div id = "app">
    <li>{{sum}}</li>
  </div>
<script>
  let vm = new Vue({
    el: '#app',
    data: {
      one: 1,
      two: 2
    },
    computed:{
      sum(){
        return this.one + this.two 
      }
    }
  })
  setTimeout(() => {
    vm.two = 3
  }, 3000)
  </script> 
</body>

one和two是在data里定义的属性,在初始化数据时做了劫持(Object.defineProperty)。computed也做了初始化,函数形式不会重写get方法,只是定义了watcher,同时利用Object.defineProperty将计算属性定义在vm上做了劫持(这里的劫持和data的劫持是不一样的)。渲染时会访问sum,它会去执行sum函数,执行函数时会访问one和two,触发get劫持,会将当前的渲染watcher(因为在$mount时,Dep.target是渲染watcher),添加到自己的dep中,同时渲染watcher会将one和two也存起来,返回其值,此时sum变为3

在3秒后,改变two的值,触发two的set方法,如果与上次的值不一样,执行notify方法就会通知dep里存的watcher去更新,此时,two的dep里只有渲染watcher,执行渲染watcher的update方法更新视图

  • 对象形式
<body>
  <div id = "app">
    <li>{{sum}}</li>
  </div>
<script>
  let vm = new Vue({
    el: '#app',
    data: {
      one: 1,
      two: 2
    },
    computed:{
      sum: {
        get: function(){
          return this.one + this.two 
        }
      }
    }
  })
  setTimeout(() => {
    vm.two = 3
  }, 3000)
  </script> 
</body>

与函数形式不同的是,如果是对象形式,在初始化计算属性时会重写get方法,渲染时会访问sum,会被get劫持,如果dirtytrue,就会调用计算属性watcherevaluate方法进行重新求值,同时会将dirty设为false,表示已经取过值,此处注意,在重新求值时会调用自定义的get方法,此时是计算属性watcher,此时全局的栈中有两个watcher[渲染watcher,计算属性watcher],在访问one和two时,会将计算属性watcher添加到每个属性dep中,此时计算属性watcher中有one,two两个dep,重要一步:经过计算后,全局还有一个渲染watcher,所以,得将渲染watcher存放在one和two的watcher队列中[计算属性watcher,渲染watcher],如果缺少这一步,在后面更改one或者two值时将不会更新视图,得到计算的值,此时sum为3

在3秒后,改变two的值,触发two的set方法,如果与上次的值不一样,执行notify方法就会通知dep里存的所有watcher去更新,也就是执行update方法,首先执行的是计算属性watcher的update方法,他会将dirty设置true,代表值发生变化,接着执行渲染watcher的update方法,此时会更新视图,更新视图是会再次访问sum,由于dirty为true,所以会再次执行evaluate方法进行重新求值,得到计算值为4

注意点

  • 计算属性可以是一个函数,也可以是对象,当为对象时,必须设置get函数
  • 计算属性的命名不能和data、methods、prop重名,在初始化时会做校验,也就是说,可以用watch去监听计算属性的

总结

计算属性有两种书写方式,一种是对象形式,一种是函数形式。重点在于对象形式的watcher收集,在重写get方法时在经过计算后会将渲染watcher添加到每个依赖的值中,那么计算属性依赖的值会收集计算属性watcher和渲染watcher两个,在改变其中一个依赖值时,首先会触发计算属性watcher的update方法进行更新值,之后在调用渲染watcher的update方法更新视图

系列链接

【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理
【Vue2.x原理剖析六】diff算法原理
【Vue2.x原理剖析七】组件原理