【Vue v2.5.17源码学习】computed

173 阅读5分钟

初始化

在new Vue() 创建vue示例时,会调用_init()方法,在这个方法里面,会再调用initState方法进行初始化。初始化Computed的initComputed方法就是在这里面调用的。

initComputed:

  1. Object.create(null)创建一个盛放computed对象中的属性的一个对象。同时把这个对象赋值给 vm._computedWatchers。
  2. 遍历computed中的所有属性,获取属性的值,如果值是function就直接获取,如果不是function就获取值的get方法。所以computed是支持有两种写法:
computed:{
  a () {
    return 'aaa'
  },
  b: {
    get () {
        return 'bbb'
    }
  }
}
  1. 如果不是SSR,为每个属性new Watcher, 并把每个属性的值或是get方法,作为参数创建实例:
const computedWatcherOptions = { lazy: true }
if (!isSSR) {
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    )
  }

这里的Watcher实例和其他地方的不同,这里只是创建了一个用来收集依赖的Dep实例。

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
 // 省略...
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  this.deps = []
  this.newDeps = []
  this.depIds = new Set()
  this.newDepIds = new Set()
  // 省略...
  this.value = this.lazy
    ? undefined
    : this.get()
}
  1. 判断属性是否在data和prop中是否存在,如果不存在,就调用defineComputed方法给每个属性设置访问器属性的get和set方法。set方法就是当属性值不是function时,如果有set方法,就把它设为访问器属性的set方法,如果没有或是属性值是function,就把访问器的set方法设置为空函数。
  2. 如果不是SSR, get方法就是createComputedGetter函数的返回值,createComputedGetter返回一个名叫computedGetter的函数。
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
    }
  }
}

以上初始化完成,简言之就是先给每个computed属性创建一个观察者实例,当然这个观察者实例与其他的不同,这里的只是创建了一个Dep实例。没有获取值。然后给每个属性设置get和set方法。

执行

<div>{{compA}}</div>

data () {
  return {
    a: 1
  }
},
computed: {
  compA () {
    return this.a + 1
  }
}

以上面的代码为例:

  1. 模板编译过程中,会对compA取值,这是就触发了compA的get方法,这个get方法就是刚才初始化时,createComputedGetter返回的computedGetter方法。
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
  if (watcher.dirty) {
    watcher.evaluate()
  }
  if (Dep.target) {
    watcher.depend()
  }
  return watcher.value
}
evaluate () {
  this.value = this.get()
  this.dirty = false
}

如果dirty是true(其实默认就是true),调用watcher.get()方法,并且把dirty设置为false,再获取该computed属性时不再进行计算,直接返回缓存的结果,缓存的位置在watcher.value。

那么watcher.get()方法又执行了什么呢?见一下代码:

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}
export function popTarget () {
  Dep.target = targetStack.pop()
}

get方法中,首先调用pushTarget方法,如果Dep.target存在,就把它放到targetStack中,这时的Dep.target就是计算属性computed的watcher。

然后继续执行下面的逻辑,就是调用当前watcher的getter方法,这个getter方法其实是在创建watcher实例时的第二个参数,来看下

function initComputed (vm: Component, computed: Object) {
  // 省略...
  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
      )
    }
    // 省略...
}

其实就是compA的方法。

computed: {
  compA () {
    return this.a + 1
  }
}

当执行compA的方法时,会获取数据对象的a,我们知道,a是响应式的,当获取a的值时,会被a的访问器属性中的get方法拦截到,执行get方法,最后返回值给compA。

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    // 省略...
})
// dep.js
 depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
// watcher.js
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.target是computed属性compA的watcher,然后a的Dep调用depend方法,再调用watcher.addDep经过优化后把compA的watcher收集到a的筐里。

这样,a的筐里有了computed属性compA的watcher。继续执行就来到了finally里。

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

popTarget方法被调用,把watcher.get方法最开始设置的Dep.target还原回去了,也就是渲染函数了。然后调用this.cleanupDeps()清空。最后返回计算后的value值。

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

把dirty设为false,evaluate就执行完了。

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

然后,再执行

if (Dep.target) {
  watcher.depend()
}

这时的Dep.target就又是渲染函数了,那computed属性的watcher调用depend又发生什么了呢?

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

我们知道watcher的deps存放所有对watcher所依赖的响应式数据,以computed中compA的watcher为例子,它的deps里肯定存有响应式数据a的dep,这样a的dep必定会调用depend方法,那这个方法做了什么?

//dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

它把Dep.target放入了a的依赖收集器里了,而这时的Dep.target就是渲染函数。这样就把a与渲染函数的watcher建立了关联,当a变化时,会直接通知渲染函数的watcher进行更新了。

当修改了依赖的数据

如果修改了a的值,那么就会调用a的set方法

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 省略...
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })

这里获取到值新值之后,调用dep.notify()方法。

notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

对a收集的所以观察者watcher执行update方法.

// watcher.js
 update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

a的观察者集合subs里,存有compA的watcher,也存在着渲染函数的watcher,如果是compA的watcher,先判断是不是计算属性的,如果是,就把dirty设置为true,证明计算属性compA所依赖的数据有更新,需要重新计算compA的值了。如果不是计算属性的watcher,就把这个watcher放入渲染页面的队列里,等待下次任务执行前,计算渲染页面。

在计算渲染页面时,必定是要获取compA的值的,这时又被compA的get函数拦截,

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

这时的watcher.dirty就是true了,又在重新计算compA的值了。往下走的话,也不用担心重复收集watcher,因为addDep方法会校验有没重复收集。

总结

<div>{{compA}}</div>

data () {
  return {
    a: 1
  }
},
computed: {
  compA () {
    return this.a + 1
  }
}
  1. 为每个computed属性创建watcher实例。
  2. 为每个computed属性设置get,set方法。
  3. 模版取计算属性值会调用计算属性的get方法,获取值并把dirty设为false,对数据缓存。同时在计算值的时候,需要获取data属性时,又触发data的get方法,这时,data再把computed的watcher收集起来。然后再收集起渲染函数的watcher。
  4. 当data变化时,会notify计算属性的watcher和渲染函数的watcher,计算属性的watcher调用update方法,只是把dirty设置为true,就是说要重新计算了。渲染函数的watcher的update方法是把渲染函数的更新放入到队列里,等待下次事件循环的时候执行。 5.在渲染函数执行过程中,肯定会获取compA的值,这样又触发了compA的get方法,就又调用重新计算compA的方法并返回新的值了。

终于缕顺了,真不容易,赶紧记下来。😄