vue源码分析之计算属性

2019-09-14

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

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

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

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

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

code

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

code2

这里要给当前页面实例上新增一个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

code3

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

// 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()

code0

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

code01

首先是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)
    }
  }

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

原文链接:segmentfault.com

上一篇:gulp-reloader
下一篇:如何在Vue Router中应用中间件
相关教程
关注微信

扫码加入 JavaScript 社区

相关文章

首次访问,需要验证
微信扫码,关注即可
(仅需验证一次)

欢迎加入 JavaScript 社区

号内回复关键字:

回到顶部