乐趣区

vue源码分析之计算属性

最近总被问道 vue 的计算属性原理是什么、计算属性是如何做依赖收集的之类的问题,今天用了一天时间好好研究了下源码,把过程基本捋顺了。总的来说还是比较简单。

先明确一下我们需要弄清楚的知识点:

  1. computed 属性如何初始化
  2. 响应式属性的变化如何引起 computed 的重新计算

弄清楚以上两点后对 computed 就会有一个比较全面的了解了。

首先,需要弄明白响应式属性是怎么实现的,具体我会在其他文章中写,这里了解个大概就可以。在代码中调用 new Vue() 的过程实际调用了定义在原型的 _init(),在这个方法里会初始化 vue 的很多属性,这其中就包括建立响应式属性。它会循环定义在data 中的所有属性值,通过 Object.defineProperty 设置每个属性的访问器属性。

因此在这个阶段,data中的属性值在获取或者赋值时就能被拦截。紧接着就是初始化 computed 属性:

这里要给当前页面实例上新增一个 computedWatchers 空对象,然后循环 computed 上的属性。在 vue 的文档里关于 computed 介绍,它既可以是函数,也可是是对象,比如下面这种:

new Vue({
    computed:{amount(){return this.price * this.count}
    }
    // 也可以写成下面这种
    computed:{
        amount:{get(){return this.price * this.count},
            set(){}
        }
    }
})

但因为不建议给 computed 属性赋值,因此比较常见的都是上面那种。所以在上图的源码中,userDefgetter 都是函数。之后就是判断是否是服务端渲染,不是就实例化一个 Watcher 类。那接着来看一下实例化的这个类是什么。源码太长了我就只展示 constructor 里的内容。

constructor(vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
    this.vm = vm
    if (isRenderWatcher) {vm._watcher = this}
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {this.deep = this.user = this.lazy = this.sync = false}
    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.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths.' + 'For full control, use a function instead.', vm)
      }
    }
    this.value = this.lazy ? undefined : this.get()}

在这个阶段做了这么几件事情:

  1. 向页面实例的 watchers 属性中依次 push 了每一个计算属性的实例。
  2. 将实例化类时传入的第二个参数(也就是上文提及的getter)设置为this.getter
  3. this.value设置为undefined

到这里为止,计算属性的初始化就完成了,如果给生命周期打了断点,你就会发现这些步骤就是在 created 之前完成的。但是到现在,vue 只是创建了响应式属性和把每一个计算属性用 watcher 实例化,并没有完成计算属性的依赖收集。

紧接着,vue 会调用原型上的 $mount 方法,这里会返回一个函数mountComponent

这里关注一下这部分代码:

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(
    vm,
    updateComponent,
    noop,
    {before() {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')
        }
      }
    },
    true /* isRenderWatcher */
  )

在挂载阶段,会再次实例化一次 Watcher 类,但是这里和之前实例的类不一样的地方在于,他的初始化属性 isRenderWatcher 为 true。所以区分一下就是,前文所述的循环计算属性时实例化的 WatchercomputedWatcher,而这里的则是renderWatcher。除了从字面上能看出他们之间的区别外。在实例化上也有不同。

// 不同一
if (isRenderWatcher) {vm._watcher = this}
// 不同二
 this.dirty = this.lazy // for lazy watchers
// 不同三
this.value = this.lazy ? undefined : this.get()

renderWatcher会在页面实例上新增一个 _watcher 属性,并且 dirty 为 false,最重要的是这里会直接调用实例上的方法get()

这块代码就比较重要了,我们一点一点说。

首先是 pushTarget(this)pushTarget 方法是定义在 Dep 文件里的方法,他的作用是往 Dep 类的自有属性 target 上赋值,并且往 Dep 模块的 targetStack 数组 push 当前的 Watcher 实例。因此对于此时的 renderWatcher 而言,它的实例被赋值给了 Dep 类上的属性。

接下来就是调用当前 renderWatcher 实例的 getter 方法,也就是上面代码中提到的 updateComponent 方法。

updateComponent = () => {vm._update(vm._render(), hydrating)
}

这里涉及到虚拟 dom 的部分,我不在这里详说,以后会再分析。因此现在对于页面来说,就是将 vue 中定义的所有 data,props,methods,computed 等挂载在页面上。为了页面正常显示,当然是需要获取值的,上文中所说的为 data 的每个属性设置 getter 访问器属性,这里就能用到。再看下 getter 的代码:

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
}

Dep.target上现在是有值的,就是 renderWatcher 实例,dep.depend就能被顺利调用。来看下 dep.depend 的代码:

  depend() {if (Dep.target) {Dep.target.addDep(this)
    }
  }

这里调用了 renderWatcher 实例上的 addDep 方法:

  /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

代码看起来可能不是很清晰,实际上这里做了三件事:

  1. 如果该 renderWatcher 实例的 newDepIds 属性不存在当前正在处理的 data 属性的 id,则添加
  2. 将当前 data 属性的 Dep 实例添加到 renderWatchernewDeps属性中
  3. 调用当前 data 属性的 Dep 实例上的方法dep.addSub
  //  添加订阅
  addSub(sub: Watcher) {this.subs.push(sub)
  }

所以第三步就是在做依赖收集的工作。对于这里,就是为每一个响应式属性添加了 updateComponent 依赖,这样修改响应式属性的值就能够引起页面的重新渲染,也就是 vnodepatch过程。

相应的,computed属性也会被渲染在页面上而被调用,和 data 属性的原理一样,computed也有访问器属性的设置,在第二张图中,调到的 defineComputed 方法:

export function defineComputed(target: any, key: string, userDef: Object | Function) {const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {sharedPropertyDefinition.get = userDef.get ? (shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get)) : noop
    sharedPropertyDefinition.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)
}

sharedPropertyDefinition是一个通用的访问器对象:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

因此当调用计算属性的时候,就是在调用计算属性上绑定的函数。这里在给 get 赋值时调用了另一个函数createComputedGetter

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

这部分代码做的事情就很有意思了,和 renderWatcher 调用 get 做的类似,watcher.evaluate方法会间接调用 computedWatcherget方法,然后调用计算属性上的函数,因为计算属性会根据不同的响应式属性而返回值,调用每一个响应式属性都会触发 getter,因此和计算属性相关的响应式属性的 Dep 实例上会订阅计算属性的变化。

说到这,计算属性的依赖收集就做完了。在这之后如果修改了某一个和计算属性绑定的响应式属性,就会触发setter

    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()
      }
      // #7981: for accessor properties without setter
      // 用于没有 setter 的访问器属性
      if (getter && !setter) return
      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()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order

      // 关闭异步,则 subs 不在调度中排序
      // 为了保证他们能正确的执行,现在就带他们进行排序
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
  }
}
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    debugger
    /* istanbul ignore else */
    if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()
    } else {queueWatcher(this)
    }
  }

对于计算属性,会重复上面的逻辑,直到新的页面渲染完成。

退出移动版