Vue2.0源码分析:响应式原理(上)

1,403 阅读7分钟

Vue2.0源码分析

如果觉得写得不错,请到GitHub我一个Star

上一篇:Vue2.0源码分析:Rollup构建,目录设计和整体流程
下一篇:Vue2.0源码分析:响应式原理(下)

介绍

在上一章节,我们分析过initState()方法的整体流程,知道它会处理propsmethodsdata等等相关的内容:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

那么我们的深入响应式原理介绍会以initState()方法开始,逐步分析Vue中响应式的原理,下面这张图可以很好的展示响应式的原理。

前置核心概念

Object.defineProperty介绍

也许你已经从很多地方了解到,Vue.js利用了Object.defineProperty(obj, key, descriptor)方法来实现响应式,其中Object.defineProperty()方法的参数介绍如下:

  • obj:要定义其属性的对象。
  • key:要定义或修改属性的名称。
  • descriptor:要定义或修改属性的描述符。

其中descriptor有很多可选的键值, 然而对Vue响应式来说最重要的是getset方法,它们分别会在获取属性值触发getter和设置属性值的时候触发setter。在介绍原理之前,我们来使用Object.defineProperty()来实现一个简单的响应式例子:

function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      console.log('get msg')
      return val
    },
    set: function reactiveSetter (newVal) {
      console.log('set msg')
      val = newVal
    }
  })
}
const vm = {
  msg: 'hello, Vue.js'
}
let msg = ''
defineReactive(vm, 'msg', vm.msg)
msg = vm.msg          // get msg
vm.msg = 'Hello, Msg' // set msg
msg = vm.msg          // get msg

为了在别的地方方便的使用Object.defineProperty()方法,因此我们把其封装成一个defineReactive函数。

proxy代理

在我们的开发过程中,我们经常会直接使用this.xxx的形式直接访问props或者data中的值,这是因为Vuepropsdata默认做了proxy代理。关于什么是proxy代理,请先看一个简单的例子:

this._data = {
  name: 'AAA',
  age: 23
}
// 代理前
console.log(this._data.name) // AAA
proxy(vm, '_data', key)
// 代理后
console.log(this.name)       // AAA

接下来我们详细介绍proxy()方法是如何实现的,在instance/state.js文件中定义了proxy方法,它的代码也很简单:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

我们可以从上面的代码中发现,proxy方法主要是做了属性的getset方法劫持。

const name = this.name
this.name = 'BBB'
// 等价于
const name = this._data.name
this._data.name = 'BBB'

$options属性

在之前的介绍中,我们知道当我们初始化Vue实例的时候传递的options会根据不同的情况进行配置合并,关于具体的options合并策略我们会在之后的章节详细介绍,现阶段我们只需要知道$options可以拿到合并后的所有属性,例如propsmethods以及data等等。

假设我们定义了如下实例:

const vm = new Vue({
  el: '#app',
  props: {
    msg: ''
  },
  data () {
    return {
      firstName: 'AAA',
      lastName: 'BBB',
      age: 23
    }
  },
  methods: {
    sayHello () {
      console.log('Hello, Vue.js')
    }
  },
  computed: {
    fullName () {
      return this.firstName + this.lastName
    }
  }
})

那么我们在之后可以通过下面的方式来取这些属性。

const opts = this.$options
const props = opts.props
const methods = opts.methods
const data = opts.data
const computed = opts.computed
const watch = opts.watch
// ...等等

props处理

介绍完以上前置核心概念后,我们第一个要学习的就是Vue.js是如何处理与props相关的逻辑的。我们把与props相关的逻辑主要分成三个部分,分别是props规范化、props初始化和props更新。

props规范化

在了解规范化之前,我们先来列举一下在日常的开发过程中,我们主要有如下几种撰写组件props的方式:

  • 数组形式:props可以写成一个数组,但数组中的key元素必须为string类型。
export default {
  props: ['name', 'age']
}
  • 键值不为对象:此种方式常见于只需要定义key类型的props
export default {
  props: {
    name: String
  }
}
  • 规范格式:此种方式是Vue.js接受props最好的格式,对于一个有很高要求的组件来说,它通过会撰写很严格的props规则,这在各个开源UI框架中是最常见的。
export default {
  props: {
    name: {
      type: String,
      default: ''
    },
    age: {
      type: Number,
      default: 0,
      validator (value) {
        return value >= 0 && value <= 100
      }
    }
  }
}

props规范化所做的事情,就是把各种不是规范格式的形式,规范化为规范格式,方便Vue.js在后续的过程中处理props。那么接下来,我们就来分析Vue.js是如何对props规范化的。

props规范化的过程发生在this._init()方法中的mergeOptions合并配置中:

import { mergeOptions } from '../util/index'
export function _init (Vue) {
  const vm = this
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

其中mergeOptions()方法是定义在src/core/util/options.js文件中,它在其中有一段这样的方法调用:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 省略代码
  normalizeProps(child, vm)
  return options
} 

我们可以发现,规范化props的代码,主要集中在normalizeProps()方法中,那么接下来我们详细分析normalizeProps()方法:

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

为了更好的理解normalizeProps()方法,我们来撰写几个案例来详细说明:

  • 数组形式:当props是数组时,会首先倒序遍历这个数组,然后使用typeof来判断数组元素的类型。如果不是string类型,则在开发环境下报错,如果是string类型,则先把key转化为驼峰形式,然后把这个key赋值到临时的res对象中,此时的键值固定为{ type: null }
// 规范化前
export default {
  props: ['age', 'nick-name']
}

// 规范化后
export default {
  props: {
    age: {
      type: null
    },
    nickName: {
      type: null
    }
  }
}
  • 对象形式:当为对象时会使用for-in遍历对象,紧接着和数组形式一样使用camelize来把key转成驼峰形式,然后使用isPlainObject()方法来判断是否为普通对象。如果不是,则转成{ type: Type }对象形式,其中Type为定义key时的Type,如果是,则直接使用这个对象。
// 规范化前
export default {
  props: {
    name: String,
    age: Number
  }
}

// 规范化后
export default {
  props: {
    name: {
      type: String
    },
    age: {
      type: Number
    }
  }
}
  • 既不是数组形式也不是对象形式:报错
// 报错:Invalid value for option "props": expected an Array or an Object,but got String
export default {
  props: 'name, age'
}

props初始化

在了解了props规范化后,我们紧接着来了解一下props初始化的过程。props初始化过程同样是发生在this._init()方法中,它在initState的时候被处理:

export function initState (vm) {
  // 省略代码
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
}

然后我们来详细看一下initProps中的代码:

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

在仔细阅读initProps()方法后,我们可以对initProps()方法进行总结,它主要做三件事情:props校验和求值props响应式props代理

props响应式

我们先来看看最简单的props响应式,这部分的过程主要使用了我们在之前介绍过的defineReactive方法:

defineReactive(props, key, value, () => {
  if (!isRoot && !isUpdatingChildComponent) {
    warn(
      `Avoid mutating a prop directly since the value will be ` +
      `overwritten whenever the parent component re-renders. ` +
      `Instead, use a data or computed property based on the prop's ` +
      `value. Prop being mutated: "${key}"`,
      vm
    )
  }
})

唯一值得注意的地方就是:在开发环境下,props的响应式劫持了setter方法,这样做的是为了保证props为单项数据流:既我们不能在子组件中直接修改父组件传递的props值。

props代理

经过props响应式后,我们会在实例上得到this._props对象,为了方便我们更好的获取props的值,我们需要对props做一层proxy代理。关于proxy的实现,我们已经在之前的章节中介绍过了。

this._props = {
  name: '',
  age: 0
}

// 代理前
console.log(this._props.name)
proxy(vm, `_props`, key)
// 代理后
console.log(this.name)

props校验求值

最后我们来看稍微复杂一点的props校验求值,这部分的功能发生在validateProp,它的代码如下:

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // boolean casting
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}

代码分析:我们可以从以上代码中发现,validateProp虽然说的是带有校验的功能,但它并不会抛出错误进而阻止validateProp()方法返回value,而是根据校验的过程中的不同情况尽可能的提示出很清晰的提示。实质上validateProp()方法最主要的还是返回value,同时也根据不同的props写法处理不同的情况。我们可以将validateProp()方法进行总结,它主要做如下几件事情:

  • 处理Boolean类型的props
  • 处理default默认数据。
  • props断言。

那么我们接下来将分别对这几件事情进行详细的描述。

处理Boolean类型

我们先来看几个props传递Boolean的例子:

// Component A
export default {
  props: {
    fixed: Boolean
  }
}

// Component B
export default {
  props: {
    fixed: [Boolean, String]
  }
}

// Component C
export default {
  props: {
    fixed: []
  }
}

然后回到源码中处理Boolean类型getTypeIndex的地方,这个函数的代码如下:

function getTypeIndex (type, expectedTypes): number {
  if (!Array.isArray(expectedTypes)) {
    return isSameType(expectedTypes, type) ? 0 : -1
  }
  for (let i = 0, len = expectedTypes.length; i < len; i++) {
    if (isSameType(expectedTypes[i], type)) {
      return i
    }
  }
  return -1
}

这个函数的实现逻辑比较清晰:

  1. Component A组件为例,它的props不是一个数组但却是Boolean类型,因此返回索引0
  2. Component B组件为例,因为它的props都是一个数组,所以要遍历这个数组,然后返回Boolean类型在数组中的索引i
  3. Component C组件为例,虽然它是一个数组,但数组中没有任何元素,因此返回索引-1

在拿到booleanIndex后,我们需要走下面这段代码逻辑:

const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
  if (absent && !hasOwn(prop, 'default')) {
    value = false
  } else if (value === '' || value === hyphenate(key)) {
    // only cast empty string / same name to boolean if
    // boolean has higher priority
    const stringIndex = getTypeIndex(String, prop.type)
    if (stringIndex < 0 || booleanIndex < stringIndex) {
      value = true
    }
  }
}

代码分析:

  • if条件判断中absent代表虽然我们在子组件中定义了props,但是父组件并没有传递任何值,然后&条件又判断了子组件props有没有提供default默认值选项,如果没有,那么它的值只能为false
// 父组件未传递fixed
export default {
  name: 'ParentComponent'
  template: `<child-component />`
}

// 子组件fixed值取false
export default {
  name: 'ChildComponent',
  props: {
    fixed: Boolean
  }
}
  • else if条件判断中,我们判断了两种特殊的props传递方式:
// Parent Component A
export default {
  name: 'ParentComponentA',
  template: `<child-component fixed />`
}

// Parent Component B
export default {
  name: 'ParentComponentB',
  template: `<child-component fixed="fixed" />`
}

对于第一个种情况stringIndex-1booleanIndex0,因此value的值为true。对于第二种情况,则需要根据props的定义具体区分:

// Child Component A
export default {
  name: 'ChildComponentA'
  props: {
    fixed: [Boolean, String]
  }
}

// Child Component B
export default {
  name: 'ChildComponentB',
  props: [String, Boolean]
}
  1. 对于ChildComponentA来说,由于stringIndex值为1booleanIndex值为0booleanIndex < stringIndex因此我们可以认为Boolean具有更高的优先级,此时value的值为true
  2. 对于ChildComponentB来说,由于stringIndex值为0booleanIndex值为1stringIndex < booleanIndex因此我们可以认为String具有更高的优先级,此时value的值不处理。
处理default默认数据

处理完Boolean类型后,我们来处理默认值,既我们提到过的虽然子组件定义了props,但父组件没有传递的情况。

// 父组件未传递fixed
export default {
  name: 'ParentComponent'
  template: `<child-component />`
}

// 子组件提供了default选项
export default {
  name: 'ChildComponent',
  props: {
    fixed: {
      type: Boolean,
      default: false
    }
  }
}

对于以上案例会走如下代码的逻辑:

if (value === undefined) {
  value = getPropDefaultValue(vm, prop, key)
}

function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
  // no default, return undefined
  if (!hasOwn(prop, 'default')) {
    return undefined
  }
  const def = prop.default
  // warn against non-factory defaults for Object & Array
  if (process.env.NODE_ENV !== 'production' && isObject(def)) {
    warn(
      'Invalid default value for prop "' + key + '": ' +
      'Props with type Object/Array must use a factory function ' +
      'to return the default value.',
      vm
    )
  }
  // the raw prop value was also undefined from previous render,
  // return previous default value to avoid unnecessary watcher trigger
  if (vm && vm.$options.propsData &&
    vm.$options.propsData[key] === undefined &&
    vm._props[key] !== undefined
  ) {
    return vm._props[key]
  }
  // call factory function for non-Function types
  // a value is Function if its prototype is function even across different execution context
  return typeof def === 'function' && getType(prop.type) !== 'Function'
    ? def.call(vm)
    : def
}

代码分析:

  1. 首先判断了子组件有没有提供default默认值选项,没有则直接返回undefined
  2. 随后判断了default如果是引用类型,则提示必须把default写成一个函数,既:
default: {}
default: []

// 必须写成
default () {
  return {}
}
default () {
  return []
}
  1. 最后再根据default的类型来取值,如果是函数类型则调用这个函数,如果不是函数类型则直接使用。
  2. 其中下面一段代码在这里我们并不会说明和分析它的具体作用,而是会在props更新章节来介绍。
if (vm && vm.$options.propsData &&
  vm.$options.propsData[key] === undefined &&
  vm._props[key] !== undefined
) {
  return vm._props[key]
}
props断言

最后我们来分析一下props断言。

function assertProp (
  prop: PropOptions,
  name: string,
  value: any,
  vm: ?Component,
  absent: boolean
) {
  if (prop.required && absent) {
    warn(
      'Missing required prop: "' + name + '"',
      vm
    )
    return
  }
  if (value == null && !prop.required) {
    return
  }
  let type = prop.type
  let valid = !type || type === true
  const expectedTypes = []
  if (type) {
    if (!Array.isArray(type)) {
      type = [type]
    }
    for (let i = 0; i < type.length && !valid; i++) {
      const assertedType = assertType(value, type[i])
      expectedTypes.push(assertedType.expectedType || '')
      valid = assertedType.valid
    }
  }

  if (!valid) {
    warn(
      getInvalidTypeMessage(name, value, expectedTypes),
      vm
    )
    return
  }
  const validator = prop.validator
  if (validator) {
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      )
    }
  }
}

assertProp中我们有三种情况需要去断言:

  • required:如果子组件props提供了required选项,代表这个props必须在父组件中传递值,如果不传递则抛出错误信息Missing required prop: fixed
  • 对于定义了多个type的类型数组,则我们会遍历这个类型数组,只要当前props的类型和类型数组中某一个元素匹配则终止遍历。,否则抛出错误提示信息。
// Parent Component
export default {
  name: 'ParentComponent',
  template: `<child-component :age="true" />`
}
// Chil Component
export default {
  name: 'ChilComponent',
  props: {
    age: [Number, String]
  }
}

// 报错:Invalid prop: type check failed for prop age,Expected Number, String,got with value true
  • 用户自己提供的validator校验器我们也需要进行断言:
// Parent Component
export default {
  name: 'ParentComponent',
  template: `<child-component :age="101" />`
}
// Chil Component
export default {
  name: 'ChilComponent',
  props: {
    age: {
      type: Number,
      validator (value) {
        return value >=0 && value <=100
      }
    }
  }
}

// 报错:Invalid prop: custom validator check failed for prop age

props更新

我们都知道子组件的props值来源于父组件,当父组件值更新时,子组件的值也会发生改变,同时触发子组件的重新渲染。我们先跳过父组件的具体编译逻辑,直接看父组件的值更新,改变子组件props值的步骤:

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // 省略代码
  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }
}

代码分析:

  1. 以上vm实例为子组件,propsData为父组件中传递的props的值,而_propKeys是之前props初始化过程中缓存起来的所有的props的key。
  2. 在父组件值更新后,会通过遍历propsKey来重新对子组件props进行校验求值,最后赋值。

以上代码就是子组件props更新的过程,在props更新后会进行子组件的重新渲染,这个重新渲染的过程分两种情况:

  • 普通props值被修改:当props值被修改后,其中有段代码props[key] = validateProp(key, propOptions, propsData, vm)根据响应式原理,会触发属性的setter,进而子组件可以重新渲染。
  • 对象props内部属性变化:当这种情况发生时,并没有触发子组件prop的更新,但是在子组件渲染的时候读取到了props,因此会收集到这个propsrender watcher,当对象props内部属性变化的时候,根据响应式原理依然会触发setter,进而子组件可以重新进行渲染。

toggleObserving作用

toggleObserving是定义在src/core/observer/index.js文件中的一个函数,其代码很简单:

export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
  shouldObserve = value
}

它的作用就是修改当前模块的shouldObserve变量,用来控制在observe的过程中是否需要把当前值变成一个observer对象。

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

接下来我们来分析,在处理props的过程中,什么时候toggleObserving(true),什么时候toggleObserving(false)以及为什么需要这样处理?

function initProps (vm: Component, propsOptions: Object) {
  if (!isRoot) {
    toggleObserving(false)
  }
  // 省略defineReactive的过程
  toggleObserving(true)
}

props初始化的时候:
我们可以看到在最开始判断了当为非根实例(子组件)的时候,进行了toggleObserving(false)的操作,这样做的目的是因为:当非根实例的时候,组件的props来自于父组件。当props为对象或者数组时,根据响应式原理,我们会递归遍历子属性然后进行observe(val),而正是因为props来源于父组件,这个过程其实已经在父组件执行过了,如果不做任何限制,那么会在子组件中又重复一次这样的过程,因此这里需要toggleObserving(false),用来避免递归props子属性的情况,这属于响应式优化的一种手段。在代码最后,又调用了toggleObserving(true),把shouldObserve的值还原。

props校验的时候:
我们先来看props提供了default默认值,且默认值返回了对象或者数组。

export default {
  props: {
    point: {
      type: Object,
      default () {
        return {
          x: 0,
          y: 0
        }
      }
    },
    list: {
      type: Array,
      default () {
        return []
      }
    }
  }
}

对于以上pointlist取默认值的情况,这个时候的props值与父组件没有关系,那么这个时候我们需要toggleObserving(true),在observe后再把shouldObserve变量设置为原来的值。

export function validateProp () {
  // 省略代码
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    const prevShouldObserve = shouldObserve 
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
}

props更新的时候:
当父组件更新的时候,会调用updateChildComponent()方法,用来更新子组件的props值,这个时候其实和props初始化的逻辑一样,我们同样不需要对指向父组件的对象或数组props进行递归子属性observe的过程,因此这里需要执行toggleObserving(false)

export function updateChildComponent () {
  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    vm.$options.propsData = propsData
  }
}

整体流程图

在分析完以上所有与props相关的逻辑后,我们可以总结如下流程图。

methods处理

在分析完props相关逻辑后,我们接下来分析与methods相关的逻辑,这部分相比于props要简单得多。

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

initState()方法中,调用了initMethods()并传入了当前实例vm和我们撰写的methods。接下来,我们看一下initMethods方法具体的实现:

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

在以上代码中可以看到,initMethods()方法实现中最重要的一段代码就是:

// 空函数
function noop () {}

vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)

它首先判断了我们定义的methods是不是function类型,如果不是则赋值为一个noop空函数,如果是则把这个方法进行bind绑定,其中传入的vm为当前实例。这样做的目的是为了把methods方法中的this指向当前实例,这样我们就能在methods方法中通过this.xxx的形式很方便的访问到propsdata以及computed等与实例相关的属性或方法。

在开发环境下,它还做了如下几种判断:

  • 必须为function类型。
// 抛出错误:Method sayHello has type null in the component definition. 
//          Did you reference the function correctly?
export default {
  methods: {
    sayHello: null
  }
}
  • 命名不能和props冲突。
// 抛出错误:Method name has already been defined as a prop.
export default {
  props: ['name']
  methods: {
    name () {
      console.log('name')
    }
  }
}
  • 命名不能和已有的实例方法冲突。
// 抛出错误:Method $set conflicts with an existing Vue instance method. 
//          Avoid defining component methods that start with _ or $.
export default {
  methods: {
    $set () {
      console.log('$set')
    }
  }
}

在分析完以上initMethods流程后,我们能得到如下流程图:

data处理

Vue中关于data的处理,根实例和子组件有一点点区别,接下来我们着重分析子组件中关于data的处理过程。

export function initState (vm: Component) {
  const opts = vm.$options
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
}

在以上代码中,首先判断了opts.data,如果值为true则代表是子组件(子组件如果没有显示定义data,则使用默认值),否则代表是根实例。对于根实例而言我们不需要执行initData的过程,只要对vm._data进行observe即可。

接下来,我们详细分析initData的过程,它是定义在src/core/instance/state.js文件中的一个方法:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

虽然initData()方法的代码有点长,但我们详细观察后可以发现,其主要做的就是四件事情:类型判断取值命名冲突判断proxy代理以及observe(data)

然后,我们分别对以上几块进行详细解释:

  • 类型判断取值:对于子组件而言,由于组件可以多次复用,因此函数必须通过工厂函数模式返回一个对象,这样在组件多次复用时就能避免引用类型的问题。
// Child Component
// 抛出错误:data functions should return an object
export default {
  data: {
    msg: 'Hello, Data'
  }
}

对于data是一个函数的情况,我们调用getData方法来取值,getData方法定义如下:

export function getData (data: Function, vm: Component): any {
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

代码分析:pushTarget是一个与响应式依赖收集有关的,我们会在后续进行详细说明。getData的取值过程包裹在try/catch中,通过data.call(vm, vm)进行调用返回,如果函数调用出错,则使用handleError进行错误统一处理。

  • 命名冲突判断:由于propsmethods有更高的优先级,因此data属性的命名不能和propsmethods中的命名冲突,因为无论是propsmethods还是data最后都会反映在实例上。另外一种命名冲突,是不能以$或者_开头,因为这样很容易和实例私有方法、属性或对外暴露以$开头的方法、属性冲突。
// 1.与methods命名冲突
// 抛出错误:Method name has already been defined as a data property.
export default {
  data () {
    return {
      name: 'data name'
    }
  },
  methods: {
    name () {
      console.log('methods name')
    }
  }
}

// 2.与props命名冲突
// 抛出错误:The data property name is already declared as a prop.
//          Use prop default value instead.
export default {
  props: ['name'],
  data () {
    return {
      name: 'data name'
    }
  }
}

// 3.不能以$和_开头
export default {
  data () {
    return {
      $data: '$data'
      _isVue: true
    }
  }
}
  • proxy代理:我们在之前已经介绍过proxy代理的作用,也讲过proxy代理_props的例子,这里代理_data跟代理_props是同样的道理。
export default {
  data () {
    return {
      msg: 'Hello, Msg'
    }
  }
}
// 代理前
console.log(this._data.msg)
proxy(vm, '_data', 'msg')
// 代理后
console.log(this.msg)
  • observe(data)observe的作用是把传入值所有的属性(包括嵌套属性)递归的进行响应式defineReactive,我们会在之后的章节中详细介绍observe的实现原理,在initData中我们只要知道observe(data)会把data函数返回对象的所有属性全部变成响应式的即可。

在分析完initData的实现后,我们可以得到initData的整体流程图。

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的相关流程后,我们可以得到如下流程图

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实例。

上一篇:Vue2.0源码分析:Rollup构建,目录设计和整体流程
下一篇:Vue2.0源码分析:响应式原理(下)