2

vue源码-对于「计算属性」的理解

这是我最近学习vue源码的一个个人总结和理解,所以可能并不适合每一位读者

本文的整体脉络如下,首先尽可能去掉细节,对计算属性源码的大致实现有一个了解,然后举一例子,分别谈谈计算属性依赖收集和派发更新的流程。

  • 计算属性的源码实现
  • 举例来说,谈谈页面初次渲染时,整个依赖收集的过程
  • 举例来说,计算属性的依赖被修改时,派发更新的过程

另外推荐2个开源的vue源码分析集合

计算属性的源码实现

  • _init() --> initState() --> initComputed()
  • 1.遍历computed选项,2.实例化computed watcher 3.defineComputed()
  • defineComputed()核心就是把计算属性用Object.defineProperty包装成响应式对象,而getter就是把用户传入的函数作为getter
  • 但是准确的说,是用户传递的fn的返回值是作为计算属性getter的return值,但是除此之外计算属性在getter时还做了一些其他的操作
  • 1是watch.depend() 2.return watch.evaluate()。 也就是1.收集依赖2.把值返回
this._init() : 重点关注重点init方法中initState

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
initState() 重点关注这一句 if (opts.computed) initComputed(vm, opts.computed)
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
initComputed() 核心就是遍历computed,每次循环都实例化一个computed watch,并且用defineComputed把计算属性包装成响应式对象
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

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

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      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)
      }
    }
  }
}
defineComputed() 核心就是Object.defineProperty ,大段代码都是在给它设置get和set
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.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)
}
createComputedGetter : 计算属性的getter就是这个computedGetter,做了2件事情,1.收集render watcher作为自己的依赖,2.调用用户的那个函数作为返回值

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

到目前为止就结束了,也就是说初始化computed watcher都没有求值

直到render时,才会触发computed watcher的getter

举例来说,谈谈页面初次渲染时,整个依赖收集的过程

比如我们有一个计算属性,并且fullName是渲染在模板中的。

computed: {
    fullName(){
        return this.firstName + this.lastName
    }
}

那么页面初次渲染时,整个依赖收集的过程如下

  • render函数执行时,会读取计算属性fullName,那么会触发fullName的getter,那么就会执行到watch.depend()和return watch.evaluate()
  • 这个computed watcher的depend()会把render watcher作为依赖收集到它的subs里。
  • 这个computed watcher的evaluate()主要是把调用了用户给的那个函数,求值并返回
  • 最后值得注意的是,调用用户的函数,也就是执行了this.firstName + this.lastName ,那么也会触发他们的getter,所以他们也会把computed watcher作为依赖,收集到subs里,将来如果被修改的话,用通知subs里的watch调用update,也就是去派发更新

举例来说,计算属性的依赖被修改时,派发更新的过程

  • 当this.firstName或者this.lastName被修改时,会触发他们的setter,setter就干两个事情。1是赋值 2是派发更新
  • 所以会通知他们的subs里的watch去调用自己的update方法。其中也包括computed watcher,
  • 那么computed watcher在update方法跟普通的user watcher的update存在区别,computed watcher并不是直接推入异步更新队列,而是 this.dep.notify()发出之前计算属性所收集的依赖去派发更新,其中就包括render watcher,调用render watcher的update就会实现视图更新了
  • 注意this.getAndInvoke的作用,就是如果this.lastName和this.firstName变化了,但是经过计算之后,计算属性的值不变,那么也不会触发notify的,也就不会更新视图
update () {
 
  if (this.computed) {
    
    if (this.dep.subs.length === 0) {
 
      this.dirty = true
    } else {
    
      this.getAndInvoke(() => {
        this.dep.notify()
      })
    }
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

这也是为什么,我们说计算属性的依赖属性不被修改的话,计算属性就不会变化。因为getter就是你那个函数嘛,而且其实就算依赖变化了,只要计算之后的计算属性变,也不会触发视图更新。


Ziwei
4.1k 声望420 粉丝

前端程序员