computed计算属性原理剖析

264 阅读6分钟

上一章节我们分析了Vue.js基于Object.defineProperty实现响应式原理的核心流程,引入了WatcherDep依赖收集派发更新等概念。 # Vue2.0响应式原理剖析# 侦听属性watch原理剖析。 本章趁热打铁,分析一下Vue.js内部计算属性computed的实现原理。

computed

应用场景: 计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来。

特点:

  1. 计算属性会基于某一个响应式数据来计算,只要依赖的数据不变,计算属性的值不会重新计算(dirty缓存)

  2. 计算属性只有真正访问到才会执行计算逻辑。只定义不使用的话内部计算逻辑不会执行(lazy)

  3. 计算属性本质上也是一个watcher(computed watcher)。

  4. computed的2种使用方式:

computed: {
    sum() {
      return this.count + 1
    },
    sumCount: {
      get() {
        return this.count + 1
      },
      set() {}
    }
}

我们先来分析一下computed的初始化逻辑:

initState (vm: Component) {
  const opts = vm.$options
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch) { // watch的初始化
    initWatch(vm, opts.watch)
  }
}

initComputed函数主要做2件事:

  1. 定义一个watchers对象(vm._computedWatchers),用来收集计算属性watcher
  2. 遍历传入的computed配置,拿到用户定义的key和执行函数,为每一个key创建对应watcher
  3. 通过defineComputed函数对计算属性的key值做了一层拦截,当我们访问vm.key时,实际会访问到该key值对应的computedGetter函数,computedGetter函数是由createComputedGetter(key)创建的,该函数内部会执行一系列计算逻辑。
function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  ...
  for (const key in computed) {    
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
      )
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
    ...
  }
}

我们看一下computed watcherrender watcher的实例化有哪些不同: computed watcher: 实例化watcher传入了lazy属性,表明了这是个computed watcherdirty也为true,并且不会执行get函数。getter为用户自定义的计算属性执行函数。()

render watcher: getterupdateComponent函数,该函数在组件初始化和更新的过程中都会执行,并且第五个参数标明为渲染watcher,然后立马执行get函数。

// 计算属性watcher实例化
getter = () => {
  return this.count + 1
}
new Watcher(
  vm,
  getter,
  noop, // noop为空函数
  { lazy: true }
)

// 渲染watcher实例化
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true) // 标识是渲染watcher

class Watcher {
  constructor (
    vm: Component,
    expOrFn: Function,   
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    if (options) {
      this.lazy = !!options.lazy // lazy为true
    } 
    this.cb = cb
    this.dirty = this.lazy // for lazy watchers
    this.getter = expOrFn
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    ...
  }
 }

defineComputed函数做了如下操作: 通过Object.defineProperty做了一层拦截,当访问计算属性key值时,会执行到createComputedGetter(key)方法。通过key值我们找到之前定义在vm._computedWatchers上面的计算属性watcher,然后会执行一系列计算。计算逻辑后续在介绍。 总结: 只有用户真正访问到计算属性时,计算属性才会计算求值

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function // 只处理函数类型
) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
  } 
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

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

让我们结合一个demo来看一下计算属性的执行逻辑:

// 页面上一开始会显示2,当我们点击button按钮后,sum的取值变为了3
<div id="app">
  <button @click="changeCount">{{ sum }}</button>
</div>
<script>
  new Vue({
    el: '#app',
    data() {
      return {
        count: 1
      }
    },
    computed: {
      sum() {
        return this.count + 1
      }
    },
    methods: {
      changeCount() {
        this.count = 2
      }
    }
  })
</script>

计算属性依赖收集

  1. new Vue之后会执行初始化(..., initState),组件的挂载流程,实例化一个render watcher,并且执行watcher.get()方法。
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true) // 标识是渲染watcher

class Watcher {
  ...,
  get () {
    pushTarget(this)   // targetStack.push(target)  Dep.target = target
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // 让updateComponent函数执行
    } catch (e) {
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()  //  targetStack.pop()   Dep.target = targetStack[targetStack.length - 1]
    }
    return value
  }
}
  1. get方法首先会执行pushTarget,把render watcher添加到targetStack数组中,并将当前的Dep.targget指向render watcher
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
  1. watcher.get方法会执行this.getter方法,实际执行到render watcher传入的updateComponent方法,执行vm_render时会执行已经编译好的render函数(template -> render function),后面会有专门章节分析编译原理。
(function anonymous(
) {
  with(this){return _c('div',{attrs:{"id":"app"}},[_c('button',{on:    {"click":changeCount}},[_v(_s(sum))])])}
  })
  1. render函数中会有对计算属性sum的访问,通过vm._computedWatchers['sum']找到sum对应的计算属性watcher,判断如果当前watcherdirtytrue,执行计算逻辑(evaluate)。
const watcher = this._computedWatchers && this._computedWatchers[key]
 if (watcher) {
  if (watcher.dirty) {
    watcher.evaluate()
  }
  if (Dep.target) {
    watcher.depend()
  }
  return watcher.value
}

class Watcher {
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
}

5.上面我们说的,实例化计算属性watcher时传入的lazytrue,此时dirty的值也为true。然后执行watcher上面的evaluate函数,此时才会真正执行watcher.get函数。同第3步一样,先执行pushTarget,把computed watcher添加到targetStack数组中,此时的targetStack会有两项,然后将当前的Dep.target指向computed watcher

25.png

6.执行computed watcher传入的getter函数,此时传入的getter函数为

function () {
  return this.count + 1
}

因为count本身是响应式数据,取值会触发count的依赖收集,当前的Dep.target指向了computed watcher, 所以count对应的dep就会把computed watcher收集起来:

const dep = new Dep()
Object.defineProperty(obj, 'count', {
  get: function reactiveGetter () {
    if (Dep.target) { // 指向计算属性watcher
      dep.depend()
    }
  }
})

class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  },
  ...,
  depend () {
    if (Dep.target) { // 计算属性watcher
      Dep.target.addDep(this) // 调用watcher的addDep方法,把dep传入
    }
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
}

class Watcher {
  ...,
  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) // 调用dep的addSub,并把计算属性watcher传入
      }
    }
  }
}

最后执行popTarget操作,targetStack删除最后一项,并把Dep.target重新指向render watcher

25.png

 function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

我们来看一下computed watcher上的变化:

26.png 7. 执行完watcher.get函数后,把computed watcher上的dirty置为false

8.由于Dep.target此时指向了render watcher,接下来继续执行computed watcherdepend方法,找到computed watcher上的deps集合,遍历deps,调用每一个depdependdep.depend流程上面已经分析过了,执行完depend后,subs数组中会新增一项为render Watcher

26.png

function computedGetter () {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) { 
      watcher.depend() // 依然是 'computed watcher'
    }
    return watcher.value
  }
}

class Watcher {
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

执行完depend函数后,会返回watcher.value。至此完成了依赖收集。

计算属性派发更新

修改count,触发countset拦截,调用dep.notify,找到count对应的dep.subs数组,遍历,依次执行watcherupdate方法。这里分析一下computed watcher的更新流程: computed watcher只会把dirty重新改为true,别的什么都不做。render watcher会执行queueWatcher方法,这个流程在响应式已经分析了,此处就不做展示开了。

class Watcher {
  ...,
  update () {
    if (this.lazy) { // computed watcher
      this.dirty = true
    } else if (this.sync) { // 同步watcher
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

总结:针对计算属性而言,第一次取值时dirtytrue, 取完值后dirty置为false。如果依赖的值不发生变化,下次取值时会返回上次计算的值。由于取值函数也会访问到其依赖的值,所以依赖的值会先把computed watcher收集起来,再把render watcher收集起来。当我们修改依赖的值时,遍历收集的watcher,执行相应watcher的更新逻辑即可。