浅析computed计算属性的实现原理

  计算属性不是API,但它是Watcher类最复杂的一种实例化的使用,还是很有必要分析的。其实主要就是分析计算属性为何可以做到当它的依赖项发生改变时才会进行重新的计算,否则当前数据是被缓存的。计算属性的值可以是对象,这个对象需要传入getset方法,这种并不常用,所以这里的分析还是介绍常用的函数形式,它们之间是大同小异的,不过可以减少认知负担,聚焦核心原理实现。

一、计算属性的初始化

function initState(vm) {  // 初始化所有状态时
  vm._watchers = []  // 当前实例watcher集合
  const opts = vm.$options  // 合并后的属性
  ... // 其他状态初始化
  if(opts.computed) {  // 如果有定义计算属性
    initComputed(vm, opts.computed)  // 进行初始化
  }
  ...
}

function initComputed(vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象
  for(const key in computed) {
    const getter = computed[key]  // computed每项对应的回调函数
    watchers[key] = new Watcher(vm, getter, noop, {lazy: true})  // 实例化computed-watcher
    ...
  }
}

  其实主要就2点:

(1)若合并options中有computed,则执行 initComputed() 方法

(2)初始化方法里也就是声明一个纯净的对象,用于保存key对应的watcher。然后遍历computed,拿到其每个key对应的回调函数,然后进行 new Watcher() 将回调函数传入,并将实例保存在纯净对象上

二、计算属性的实现原理

  如上初始化的时候,其实还是按照惯例,将定义的computed属性的每一项使用Watcher类进行实例化,不过这里是按照computed-watcher的形式,来看下如何实例化的:

class Watcher{
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this._watchers.push(this)
    if(options) {
      this.lazy = !!options.lazy  // 表示是computed
    }
    this.dirty = this.lazy  // dirty为标记位,表示是否对computed计算
    this.getter = expOrFn  // computed的回调函数
    this.value = undefined
  }
}

  这里就点到为止,实例化已经结束了。并没有和之前render-watcher以及user-watcher那般,执行get方法,这是为什么?我们接着分析为何如此,补全之前初始化computed的方法:

function initComputed(vm, computed) {
  ...
  for(const key in computed) {
    const getter = computed[key]  // // computed每项对应的回调函数
    ...
    if (!(key in vm)) {
      defineComputed(vm, key, getter)
    }
    ... key不能和data里的属性重名
    ... key不能和props里的属性重名
  }
}

  这里的App组件在执行extend创建子组件的构造函数时,已经将key挂载到vm的原型中了,不过之前也是执行的defineComputed方法,所以不妨碍我们看它做了什么:

function defineComputed(target, key) {
  ...
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: createComputedGetter(key),
    set: noop
  })
}

  这个方法的作用就是让computed成为一个响应式数据,并定义它的get属性,也就是说当页面执行渲染访问到computed时,才会触发get然后执行createComputedGetter方法,所以之前的点到为止再这里会续上,看下get方法是怎么定义的:

function createComputedGetter (key) { // 高阶函数
  return function () {  // 返回函数
    const watcher = this._computedWatchers && this._computedWatchers[key] // 得到key对应的computed-watcher
    if (watcher) {
      if (watcher.dirty) {  // 在实例化watcher时为true,表示需要计算
        watcher.evaluate()  // 进行计算属性的求值
      }
      if (Dep.target) {  // 当前的watcher,这里是页面渲染触发的这个方法,所以为render-watcher
        watcher.depend()  // 收集当前watcher
      }
      return watcher.value  // 返回求到的值或之前缓存的值
    }
  }
}

class Watcher {
  ...
  evaluate () {
    this.value = this.get()  //  计算属性求值
    this.dirty = false  // 表示计算属性已经计算,不需要再计算
  }
  depend () {
    let i = this.deps.length  // deps内是计算属性内能访问到的响应式数据的dep的数组集合
    while (i--) {
      this.deps[i].depend()  // 让每个dep收集当前的render-watcher
    }
  }
}

  这里的变量watcher就是之前computed对应的computed-watcher实例,接下来会执行Watcher类专门为计算属性定义的两个方法,在执行evaluate方法进行求值的过程中又会触发computed内可以访问到的响应式数据的get,它们会将当前的computed-watcher作为依赖收集到自己的dep里,计算完毕之后将dirty置为false,表示已经计算过了。

  然后执行depend让计算属性内的响应式数据订阅当前的render-watcher,所以computed内的响应式数据会收集computed-watcherrender-watcher两个watcher,当computed内的状态发生变更触发set后,首先通知computed需要进行重新计算,然后通知到视图执行渲染,再渲染中会访问到computed计算后的值,最后渲染到页面。

  当computed内的响应式数据变更后触发的通知:
class Watcher {
  ...
  update() {  // 当computed内的响应式数据触发set后
    if(this.lazy) {
      this.dirty = true  // 通知computed需要重新计算了
    }
    ...
  }
}

  最后还是以一个示例结合流程图来帮大家理清楚这里的逻辑:

export default {
  data() {
    return {
      manName: "cc",
      womanName: "ww"
    };
  },
  computed: {
    newName() {
      return this.manName + ":" + this.womanName;
    }
  },
  methods: {
    changeName() {
      this.manName = "ss";
    }
  }
};

  总结:
  为什么计算属性有缓存功能?因为当计算属性经过计算后,内部的标志位会表明已经计算过了,再次访问时会直接读取计算后的值;
  为什么计算属性内的响应式数据发生变更后,计算属性会重新计算?因为内部的响应式数据会收集computed-watcher,变更后通知计算属性要进行计算,也会通知页面重新渲染,渲染时会读取到重新计算后的值。
posted @ 2018-06-08 23:30  古兰精  阅读(813)  评论(0编辑  收藏  举报