vue 2.0 中理解 computed

623 阅读6分钟

computed 是vue 中的计算属性,接下来会带着问题去看下他是怎么工作的。

  • 计算属性什么时候会计算
  • 计算属性是怎么知道它本身依赖于哪些属性的。以便知道其什么时候更新
  • vue官方文档的缓存计算结果怎么理解? 我们先定义一个计算属性
computed:{
  total(){
    return this.a + this.b
  }
}

在 initState 函数中来初始化 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) initWatch(vm, opts.watch)
}
function initComputed (vm: Component, computed: Object) {
  for (const key in computed) {
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && key in vm) {
      warn(
        `existing instance property "${key}" will be ` +
        `overwritten by a computed property with the same name.`,
        vm
      )
    }
    const userDef = computed[key]
    if (typeof userDef === 'function') {
      computedSharedDefinition.get = makeComputedGetter(userDef, vm)
      computedSharedDefinition.set = noop
    } else {
      computedSharedDefinition.get = userDef.get
        ? userDef.cache !== false
          ? makeComputedGetter(userDef.get, vm)
          : bind(userDef.get, vm)
        : noop
      computedSharedDefinition.set = userDef.set
        ? bind(userDef.set, vm)
        : noop
    }
    // 将计算属性定义到实例上   设置属性描述符
    Object.defineProperty(vm, key, computedSharedDefinition)
  }
}

在initComputed 中对计算属性进行遍历, 将属性定义到实例上, 对属性的值调用 makeComputedGetter 来生成计算属性的 getter

function makeComputedGetter (getter: Function, owner: Component): Function {
  // 利用函数创建的作用域形成一个闭包作用域
  const watcher = new Watcher(owner, getter, noop, {
    lazy: true
  })
  // computed 返回一个闭包作用域,值是从添加的watcher 中获取的,相当于一个缓存
  return function computedGetter () {
    // 是不是立即执行
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}

第一个问题:计算属性什么时候会计算

  1. 在模板或者其他地方用到这个值 total 的时候, 会触发 computedSharedDefinition.get
  2. 在makeComputedGetter 中 创建的 watcher 传入的初始值 lazy: true 在 Watcher内部 this.dirty = this.lazy // for lazy watchers 所以初始化的时候 watcher.dirty == true , 会调用 watcher.evaluate() ,将dirty 设置为 false ,并且调用 get 函数 执行 getter 函数得到返回值。
get () {
    pushTarget(this)
    const value = this.getter.call(this.vm, this.vm)
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
}
evaluate () {
    this.value = this.get()
    this.dirty = false
}

第二个问题:计算属性是怎么知道它本身依赖于哪些属性的?

在上面说到的get 函数中调用了Dep类中 的几个方法

Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

当代码执行到this.getter.call,实际上执行的是计算属性的函数,也就是
total() { return this.a + this.b};当代码执行到this.a时候。就会触发之前我们所讲的defineReactive内部的代码。 this.a触发了dep.depend()


import {observe} from './observer'
import Dep from './dep.js'
export default function defineReactive (data,key,val) {
  const dep = new Dep()
  if (arguments.length == 2) {
    val = data[key]
  }
  let childOb = observe(val)
  Object.defineProperty(data,key,{
    enumerable: true,  //  可被枚举
    configurable: true, // 可以被设置,比如删除
    get () {
      // 如果处于一个依赖收集阶段
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      console.log('你试图访问obj 的'+key+'属性' + val)
      return val
    },
    set (newVal) {
      console.log('你获取了'+key+'值' + newVal)
      if (val===newVal) {
        return
      }
      val = newVal
      // 当设置了新值, 需要继续添加观察
      childOb = observe(val)
      dep.notify()
    }
  })
}


  //省略代码...
  depend () {
    // 由于这里的Dep.target此时对应的是total的watcher。
    // 而这里的this是指定义this.a时,生成的dep。
    // 所以这里是告诉total依赖于this.a
    if (Dep.target) {
      // 通过调用addDep.让total的watcher知道total依赖this.a
      Dep.target.addDep(this)
    }
  }
}

class Watcher {
  // ...省略代码
  addDep (dep: Dep) {
    // 此时的this是total的watcher
    const id = dep.id
    // 防止重复收集
    if (!this.newDepIds.has(id)) {
      // 将依赖的可观察对象记录。
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // 如果这个可观察对象没有记录当前watcher,
      if (!this.depIds.has(id)) {
        // 则将当前的watcher加入到可观察对象中
        // (方便后续a变化后,告知total)
        dep.addSub(this)
      }
    }
  }
}

至此,上述的第二个问题,也有了答案。就是当生成虚拟dom的时候,用到了total,由于得到total值的watcher是脏的,需要计算一次,然后就将Dep.target的watcher设为total相关的watcher。并在watcher内执行了total函数,在函数内部,访问了this.a。this.a的getter中,通过dep.depend(),将this.a的getter上方的dep,加入到total的watcher.dep中,再通过watcher中的dep.addSub(this),将total的watcher加入到了this.a的getter上方中的dep中。至此total知道了它依赖于this.a。this.a也知道了,total需要this.a。

计算属性的依赖值发生变化时,发生了什么?

当执行 this.a += 1 的时候,此时会走this.a的setter函数。我们看看setter中所做的事情。

set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
  // 如果旧值与新值相当,什么都不做。直接返回。
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
  // 无关代码,pass
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
  // 有定义过setter的话通过setter设置新值
  if (setter) {
    setter.call(obj, newVal)
  } else {
    // 否则的话直接设置新值
    val = newVal
  }
  // 考虑新值是对象的情况。
  childOb = !shallow && observe(newVal)

  // 通知观察了this.a的观察者。
  // 这里实际上是有两个观察a的观察者
  // 一个是updateComponent。
  // 一个是这里讲的total。
  dep.notify()
}

这里我们看看dep.notify干了什么

class Dep {
  // **** 其他代码
  notify () {
    // 这里的subs其实就是上述的两个watcher。
    // 分别执行watcher的update
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

class Watcher{
  update () {
    // 第一个watcher,即关于updateComponent的。
    // 会执行queueWatcher。也就是会将处理放到等待队列里
    // 等待队列中,而第二个watcher由于lazy为true,
    // 所以只是将watcher标记为dirty。
    // 由于队列这个比较复杂,所以单开话题去讲
    // 这里我们只需要知道它是一个异步的队列,最后结果就是
    // 挨个执行队列中watcher的run方法。
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

当触发了依赖更新时候,第一个watcher(关于total的)会将自己的dirty标记为true,第二个则会执行run方法,在其中运行this.get导致updateComponent执行,进而再次计算vnode,这时会再次计算this.total。则会再次触发total的getter,这时候我们再复习一下之前讲过的这个computed的getter:

vue官方文档的缓存计算结果怎么理解?也就有了答案。也就是说计算属性只有其依赖变更的时候才会去计算,依赖不更新的时候,是不会计算的。正文这一小节提到的,total的更新是由于this.a的更新导致其setter被触发,因此通知了其依赖,即total这个watcher。如果total的不依赖于this.a,则total相关的watcher的dirty就不会变为true,也就不会再次计算了。

总结以上流程

initComputed定义了相关的计算属性相关的watcher,以及watcher的getter。
在第一次计算vnode的时候顺便执行了计算属性的计算逻辑,顺便收集了依赖。本例中total收集到了依赖a,b;并且a,b也被告知total观察了他们。当a,b任何一个改变时的时候,就会将total相关的watcher.dirty设置为true,下次需要更新界面时,计算属性就会被重新计算。当然,如果没有依赖于total的地方。那么total是不会计算的,例如total根本没被界面或者js代码用到,就不会计算total;如果total所有的依赖没有变更,其dirty为false,则也是无需计算的。