响应式原理一:data 初始化

757 阅读5分钟

对于 Vue 框架来说,数据响应式原理是它的核心特点之一,即数据发生变化,驱动视图改变。而对于了解过 Vue 的同学来说,其实现响应式的核心是利用 ES5 的 Object.defineProperty ,将普通对象定义为响应式对象,即给对象属性设置 gettersetter 方法,监听属性值的变化。

本文将来分析数据响应式原理,先从 data 开始。

initState

沿着主线将 data 的初始化逻辑整理成一张图,如下:

data-init.png

在初始化 Vue 实例时,函数 initStatedata 做了初始化操作,位于 src/core/instance/init.js,具体实现如下:

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

函数接收一个参数:Vue 实例。其作用是对 propsmethodsdatacomputedwatch 做初始化操作。而对 data 的初始化操作仅有 4 行代码:

if (opts.data) {
  initData(vm)
} else {
  observe(vm._data = {}, true /* asRootData */)
}

data 的初始化有两种场景:

  • 当执行 new Vue 实例化对象时,此时 vm.$options.dataundefined ,执行 else 逻辑;
  • 在实例化 Vue 的过程中,当执行到 patch,即将虚拟 Vnode 转换为真实 DOM 的过程中,会执行子组件初始化操作,基于父组件已经初始化的情况下,此时 vm.$options.data 不为空,执行 if 逻辑。

initData 内部实现过程中,其最后也会调用 observe ,那么来看其内部具体实现。

initData

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 */)
}

函数接收一个参数:Vue 实例,其作用是将 data 对象设置为响应式对象。

data 对外提供的数据类型有两种:一是对象;二是函数。

如果是对象的话,直接赋值给 vm._datadata;如果是函数,则执行函数返回对象赋值给 vm._data ,即调用函数 getData

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

函数接收两个参数:

  • data:用户传进来的函数
  • vm:Vue 实例

其作用是执行函数 data,返回对象,即调用 call 来执行函数的。

接着调用函数 isPlainObject 检查 data 其数据类型是否为 Object,否的话则会在开发环境中抛出告警。

然后将 data 属性代理到 vm 实例上,但在代理之前会检查其属性合法性,即 data 属性(key)是否存在于 methods 或者 props ,如果存在的话,则在开发环境中抛出告警。

如果属性不存在于 methods 或者 props,并且 key 不是以 _ 或者 $ 开头,则将其代理到 vm 实例上,具体实现如下:

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

函数接收三个参数:

  • targetvm 实例
  • sourceKeyvm 实例上属性 _data
  • key:属性名称

其作用是将 data 属性定义在 vm 实例上。

从代码实现可以看出,平时经常使用 this.xxx 时,实际上通过 this._data.xxx 返回其值的,这也就解释了为什么可以直接通过 vm 实例访问属性。

最后调用函数 observedata 对象设置为响应式对象。

observe

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
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
}

函数接收两个参数:

  • value:即将被设置为响应式对象的普通对象,此时的值为 data 对象
  • asRootData:表示 root data,其数据类型为布尔值,此时的值为 true

首先,对 value 做类型检查,如果其数据类型不是 Object 或者是 VNode 实例,则结束程序;从这里也可以看出 VNode 不能是响应式的。

接着,检查 value 是否有属性 __ob__,并且其属性 __ob__ 是否为 Observer 实例,如果两个条件都满足,则将 value.__ob__ 赋值给 ob ,说明已经设置过;否则满足其它条件时实例化 Observer 对象,其内部实现如下:

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
}

从构造器可看出,先初始化 Dep 实例,用于收集依赖。

接着在 value 定义属性 __ob__ ,其值指向 Observer 实例,具体实现如下:

/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

然后判断 value 其数据类型是否为数组,如果是数组的话,则执行 if 逻辑,否则执行 else 逻辑;先来看下数据类型为数组逻辑。

如果 hasPrototrue,则调用 protoAugment 函数,否则调用 copyAugment ,那么来看下这两个函数是如何实现的?

/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

函数接收两个参数:

  • `target :目标对象

  • src:数据类型为对象,此处传入的参数 arrayMethods 指向如下:

    /*
     * not type checking this file because flow doesn't play well with
     * dynamically accessing methods on Array prototype
     */
    
    import { def } from '../util/index'
    
    const arrayProto = Array.prototype
    export const arrayMethods = Object.create(arrayProto)
    
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    
    /**
     * Intercept mutating methods and emit events
     */
    methodsToPatch.forEach(function (method) {
      // cache original method
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        if (inserted) ob.observeArray(inserted)
        // notify change
        ob.dep.notify()
        return result
      })
    })
    

    arrayMethods原型指向Array原型,然后遍历methodsToPatch(包含数组一系列方法),将数组方法定义到对象arrayMethods` 上。

copyAugment 实现如下:

/**
 * Augment a target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

遍历 keys ,即数组七个方法(pushpopshiftunshiftsplicesortreverse),分别将它们设置到 value 上。

执行以上逻辑后,会调用 observeArray 函数,具体实现如下:

 /**
  * Observe a list of Array items.
  */
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

逻辑实现很简单,遍历数组,然后再调用 observe 将其设置为响应式属性。

最后来看 else 分支的实现逻辑,即 walk 函数的代码实现:

/**
  * Walk through all properties and convert them into
  * getter/setters. This method should only be called when
  * value type is Object.
  */
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}

获取对象 keys,对其进行遍历,调用函数 defineReactive 将属性设置为响应式属性,这也是实现响应式的核心函数,具体实现如下:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  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) {
      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
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

函数的实现使用 ES5 Object.definePropert 在对象上定义响应式属性,即定义了 getset 函数,函数的作用分别是依赖收集(换句话说,就是将观察者添加到订阅列表)和派发更新(换句话说,当数据发生变化,对已经订阅的观察者发出通知,让它们做出相应的更新)。

当获取属性值时,则会触发 get 函数;比如在渲染 vm 实例时就会触发;当改变属性值,则会触发 set 函数。

除此之外,如果 value 其数据类型为 Object,则会调用函数 observe 处理其属性,这样确保了不管对象嵌套的深度有多深,都可以将其设置为响应式属性。

至此,data 初始化过程就分析到这里。

参考链接