Vue源码解析(computed,watch)

420 阅读5分钟

系列文章:

computed处理

处理computed相关的逻辑,发生在initState中,接下来详细分析与computed相关的逻辑。

export function initState (vm: Component) {
  // 省略代码
  const opts = vm.$options
  if (opts.computed) initComputed(vm, opts.computed)
}

知道computed计算属性是依赖于其它响应式变量的,因此分析computed的时候会分为两个步骤:computed初始化computed更新

computed初始化

initState()方法中如果传递了computed,那么会调用initComputed()方法。initComputed()方法定义在src/core/instance/state.js文件中,其代码如下:

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
  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
      )
    }
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

代码分析:

  • initComputed()方法中,首先定义了一个_computedWatchers的变量,这个变量的作用是缓存当前实例所有计算属性的watcher
  • 接下来遍历所有的computed,然后对每一个computed进行类型判断,如果是function类型,那么直接使用,如果是对象则代表是get/set形式,则直接取get。如果最后获取到的computedgetternull,则在开发环境下提示错误。
// 两种类型的计算属性
export default {
  props: ['index'],
  data () {
    return {
      firstName: 'first',
      lastName: 'last'
    }
  },
  computed: {
    fullName () {
      return this.firstName + this.lastName
    },
    active: {
      get: function () {
        return this.index
      },
      set: function (newVal) {
        this.$emit('update:index', newVal)
      } 
    }
  }
}

以上面代码为例,两种类型的computed获取到的getter分别如下:

// function类型
const getter = function () {
  this.firstName + this.lastName
}

// get/set类型
const getter = function () {
  return this.index
}
  • 然后在非SSR服务端渲染的情况下,会在_computedWatchers上新建一个Watcher的实例。以上面代码为例,_computedWatchers在遍历完毕后,可以用如下代码表示:
// 当前vm实例
{
  _computedWatchers: {
    fullName: new Watcher(),
    active: new Watcher()
  }
}
  • 最后、首先判断了当前遍历的computed是否已经在vm实例上,如果不在则调用defineComputed()方法,如果在还需要判断当前遍历的computed是否和propsdata命名冲突,如果冲突则提示错误。 注意:对应子组件而言,这个时候当前遍历的computed已经在vm实例上了,所以并不会调用defineComputed()方法,从上面代码注释也能看的出来。对于子组件而言,真正initComputed的过程是发生在Vue.extend方法中:
Vue.extend = function (extendOptions) {
  // 省略代码
  const Super = this
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

  // 初始化子组件的computed
  if (Sub.options.computed) {
    initComputed(Sub)
  }
}

// extend.js中的initComputed定义
function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

然后initComputed调用的defineComputed()方法,就和现在的defineComputed()方法是同一个方法,它和此时的initComputed()方法定义在同一个位置(src/core/instance/state.js):

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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)
}

defineComputed()方法的逻辑很简单,根据不同的类型的computed,对sharedPropertyDefinitiongetset进行赋值。sharedPropertyDefinition在之前的proxy中,已经介绍过, 它就是Object.defineProperty()方法descriptor参数的一个共享配置。

在非SSR服务端渲染的情况,sharedPropertyDefinition.get的值是调用了createComputedGetter()方法,而在SSR服务端渲染的情况下是调用了createGetterInvoker()方法。在分析Vue源码的过程中,因为侧重于Web浏览器端的表现,因此接下来会分析createComputedGetter()方法的实现。createComputedGetter()方法和defineComputed()方法定义在同一个位置,代码如下:

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

可以看到createComputedGetter()方法返回了一个函数,这个函数会在获取computed的时候被调用,例如组件渲染的时候:

<template>
  <div>{{fullName}}</div>
</template>

根据以上代码再来看computedGetter()方法:组件渲染的时候会获取fullName计算属性,然后调用computedGetter()方法,在这个方法执行的时候,首先判断watcher.dirty属性,这个属性在new Watcher()的时候与传入的const computedWatcherOptions = { lazy: true }有关。在Watcher类的构造函数中,有这样一段代码:

class Watcher {
  // 省略代码
  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    if (options) {
      this.lazy = !!options.lazy
    } else {
      this.lazy = false
    }
    this.dirty = this.lazy
  }
}

因为传入的lazy值为true,因此watcher.dirty条件判断为真,进行watcher.evaluate()计算。随后判断了Dep.target为真,则进行依赖收集watcher.depend(),关于依赖收集会在之后的章节详细介绍。只要知道,当在组件渲染的时候触发的computed依赖收集,收集的是render watcher。最后,看一下watcher.evaluate()方法的实现:

class Watcher {
  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
}

evaluate()方法它的实现非常简单,就是触发computedgetter进行求值,然后把dirty设置为false

computed更新

在介绍完了computed的初始化后,再来看computed的更新过程,以下面为例:

export default {
  template: `
    <div>{{fullName}}</div>
    <button @click="change">change</button>
  `
  data () {
    return {
      total: 0,
      firstName: 'first',
      lastName: 'last'
    }
  },
  computed: {
    fullName () {
      if (this.total > 0) {
        return this.firstName + this.lastName
      } else {
        return 'pleace click'
      }
    }
  },
  methods: {
    change () {
      this.total++
    }
  }
}

因为totalfirstNamelastName全部为响应式变量,所以fullName这个计算属性初始化的时候,此时total值为0fullName计算属性有两个Watcher,其中一个是计算属性watcher,另外一个是渲染watcher。当点击按钮触发事件后,会触发total属性的setter方法,进而调用一个叫做notify的方法。

set: function reactiveSetter (newVal) {
  // 省略
  dep.notify()
}

其中notify()是定义在Dep类中的一个方法:

export default class Dep {
   constructor () {
    this.subs = []
  }
  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.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

代码分析:

  • subs就是收集起来的watcher,它是一个数组,对应上面案例的话它是一个长度为2的数组并且其中一个为render watcher
  • notify()方法调用时,会遍历subs数组,然后依次调用当前watcherupdate方法。其中update方法是定义在Watcher类中的一个实例方法,代码如下:
class Watcher {
  // 省略其它
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

当第一次遍历时,此时的watcher为计算属性watcher,已经在前面介绍过计算属性watcher它的this.lazy值为true,因此会进行this.dirty = true。 当第二次遍历时,此时的watcher为渲染watcher,对于渲染watcher而言,它的lazy值为falsethis.syncfalse,因此会调用queueWatcher()方法。目前不需要知道queueWatcher是怎么实现的,只需要知道queueWatcher()方法在调用时,会触发updateComponent()方法:

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

可以看到updateComponent()方法调用了vm._update方法,而这个方法的作用就是重新进行组件渲染,在组件渲染的过程中,会再次读取fullName的值,也就是说会调用下面这段代码:

fullName () {
  if (this.total > 0) {
    return this.firstName + this.lastName
  } else {
    return 'pleace click'
  }
}

因为此时的total值为1,所以会返回this.firstName + this.lastName的值,而firstNamelastName又是定义在data中的响应式变量,会依次触发firstNamelastNamegetter,然后进行依赖收集。在组件渲染完毕后,fullName的依赖数组subs此时会有四个watcher,分别是三个计算属性watcher和一个渲染watcher。无论这三个计算属性watcher哪一个值更新了,都会再出重复以上的流程,这就是computed更新的过程。

在分析完computed的相关流程后,可以得到如下流程图

img

watch处理

在介绍完处理computed相关的逻辑后,我们接下来看watch是如何处理的。

watch初始化

export function initState (vm: Component) {
  // 省略代码
  const opts = vm.$options
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

我们可以看到,处理watch的逻辑发生在initWatch()方法中,在这个方法调用之前,首先对watch做了判断,其中nativeWatch是定义在src/core/util/env.js中的一个常量:

// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch

然后,让我们来看一下initWatch的实现,它定义在src/core/instance/state.js文件中:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

我们可以看到,initWatch()方法的实现非常简单,首先对watch做了判断,如果是数组则遍历这个数组调用createWatcher()方法,如果不是则直接调用createWatcher()。按照watch的使用规则,我们有如下几种形式的写法:

export default {
  data () {
    return {
      age: 23,
      name: 'AAA',
      nested: {
        a: {
          b: 'b'
        }
      }
    }
  },
  watch: {
    name (newVal, oldVal) {
      console.log(newVal, oldVal)
    },
    nested: {
      handler (newVal, oldVal) {
        console.log(newVal, oldVal),
      },
      deep: true
    }
  }
}

接着,我们需要来看一下createWatcher()函数的具体实现:

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

代码分析:

  • createWatcher()方法的主要作用就是进行watch参数规范化,然后将规范化后的参数传递给vm.$watch()
  • createWatcher()中首先判断了handler参数是否为普通对象,如果是普通对象则代表是如下形式定义的watch
{
  watch: {
    nested: {
      handler (newVal, oldVal) {
        console.log(newVal, oldVal),
      },
      deep: true
    }
  }
}

此时,应该把handler赋值给可选的options参数,然后handler赋值为真正的回调函数。

  • 接着,对handler进行了类型判断,如果是string类型则把此时vm[handler]赋值给它。根据这段代码的逻辑,意味着我们可以选择把watch回调函数定义在methods中
export default {
  data () {
    return {
      name: 'AAA'
    }
  },
  watch: {
    name: 'nameWatchCallback'
  },
  methods: {
    nameWatchCallback (newVal, oldVal) {
      console.log(newVal, oldVal)
    }
  }
}
  • 最后,把规范化后的参数传递给vm.$watch()。关于$watch()何时挂载到Vue.prototype上,我们已经在之前介绍过了,它发生在stateMixin中。

在分析完createWatcher()方法实现逻辑后,我们接着来看$watch()方法的具体实现逻辑:

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

我们可以发现,$watch方法主要做两件事情:创建Watcher实例返回unwatchFn函数,接下来我们分别对这两部分的逻辑进行详细的解释。

创建Watcher实例

我们先来看一下Watcher构造函数的代码:

// 精简代码
class Watcher {
  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    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
    }
  }
}

我们从构造函数中可以看到,当实例化一个watch的时候,会根据传递的options来处理deepuserlazysync以及before属性。watcher根据不同的用法,有几种不同的分类:

  • render watcher:渲染watcher,例如当在template模板中使用{{}}语法读取一个变量的时候,此时这个变量收集的依赖就是render watcher,当这个变量值更新的时候会触发render watcher进行组件的重新渲染。是否为渲染warcher,使用构造函数参数isRenderWatchertrue进行区分。
  • computed watcher:计算属性watcher,当我们在定义计算属性的时候,计算属性收集的依赖就是另外一个或者多个变量,当其中一个变量的值发生变量,就会触发计算属性重新进行求值。是否为计算属性watcher,使用options.lazytrue进行区分。
  • user watcher:用户自定义watcher,多发生在this.$watch或者组件watch选择配置中,此时收集的依赖就是变量自身,当变量的值发生变化的时候,就会调用watch提供的回调函数。是否为用户自定义watcher,使用options.usertrue进行区分。

返回unwatchFn函数

在构造函数中可以发现,它定义了一个_watchers变量,然后在每次实例化的时候,把自身添加到这个数组中,这样做的目的是为了方便清除依赖。在之前的介绍中,我们知道$watch返回了一个unwatchFn函数,它用来取消监听。接下来,看一下teardown()方法的具体实现。

// Watcher类精简代码
class Watcher {
  constructor () {
    this.active = true
    this.deps = []
  }
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

// Dep类精简代码
class Dep {
  constructor () {
    this.subs = []
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
}

teardown()方法的实现很简单,就是从deps数组中移除当前的watcher,其中deps存储的是Dep实例。