Vue源码解析之 响应式对象

1,116 阅读4分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue 源码解析系列第 7 篇,关注专栏

前言

Vue.js 响应式的核心是利用 ES5 的 Object.defineProperty,这也是为什么 Vue.js 不兼容 IE8 及以下浏览器的原因。

/**
 * obj 定义属性的对象
 * prop 定义或修改的属性的名称
 * descriptor 将被定义或修改的属性描述符
 */
Object.defineProperty(obj, prop, descriptor)

/** 
 * descriptor 我们比较关注是 get 和 set
 * 当访问属性时,会触发 getter 方法,当修改属性时,会触发 setter 方法
 */ 
// 案例
<div id="app">{{ msg }}</div>
const app = new Vue({
    el: '#app',
    data() {
        return {
            msg: 'hello world'
        }
    },
})

响应式过程

initState

Vue 在初始化阶段,_init 方法执行时,会执行 initState(vm) 方法,它定义在 src/core/instance/state.js

// _init 定义在 src/core/instance/init.js
export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // 省略
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
    
    // 省略
  }
}

// initState
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 函数主要初始化 propsmethodsdatacomputed 和 wathcer 等,我们重点关注 initPropsinitData 方法。

initProps

initProps 过程主要是遍历 props 定义的配置,调用 defineReactive 方法将每个 prop 对应的值变成响应式,可以通过 vm._props.xxx访问到定义 props 中对应的属性。另一方面通过 proxy 把 vm._props.xxx 的访问代理到 vm.xxx 上。

// initProps 定义在 src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      // 省略
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

initData

initData 过程主要是遍历 data 返回的对象,调用 observe 方法将 data 变成响应式,可以通过vm._data.xxx 访问到定义 data 中对应的属性。另一方面通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上。

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

observe

observe 方法作用给非 VNode 的对象类型数据添加一个 Observer,如果已添加直接返回,否则根据条件添加 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
}

Observer

Observer 是一个类,它的作用是给对象的属性添加 gettersetter ,用于依赖收集和派发更新。

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

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // def 对 Object.defineProperty 封装 给对象添加__ob__属性 
    // { 'msg': 'hello world', '__ob__': {dep: ..., value: ..., vmCount: 0]}}
    def(value, '__ob__', this)
    // 如果是数组 递归调用
    if (Array.isArray(value)) { 
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      // 如果是对象 则遍历 给对象每个属性添加getter和setter 变成响应式对象
      this.walk(value) 
    }
  }

  /**
   * Walk through each property 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])
    }
  }

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

def

def 是对 Object.defineProperty 封装,它定义在 src/core/util/lang.js

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

defineReactive

defineReactive 作用就是定义一个响应式对象,给对象动态添加 gettersetter,它定义在 src/core/observer/index.js

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

总结

  • Vue 响应式主要调用 ES5 的 Object.defineProperty 方法,给对象属性添加对应的 gettersetter ,该方法不兼容 IE8 及以下浏览器。
  • Vue 初始化时会调用 initState 方法,该方法主要初始化 propsmethodsdatacomputed 和 wathcer 等,重点关注 initPropsinitData 方法。
  • 初始化 props (initProps) 会调用 defineReactive 方法将 props 每个 prop 值变成响应式。
  • 初始化 data (initData) 会调用 observe方法将 data 变成响应式。
  • observe 方法作用给非 VNode 的对象类型数据添加一个 Observer,如果已添加直接返回,否则根据条件添加 Observer 对象实例。
  • Observer 是一个类,它的作用是给对象的属性添加 gettersetter,用于依赖收集和派发更新。如果值是数组类型,则递归给每个值添加响应式,如果是对象类型,则会调用 defineReactive 方法,变成响应式对象。
  • defineReactive 方法就是定义一个响应式对象,给对象动态添加 getter 和 setter

参考

Vue.js 技术揭秘

Vue 源码解析系列

  1. Vue源码解析之 源码调试
  2. Vue源码解析之 编译
  3. Vue源码解析之 数据驱动
  4. Vue源码解析之 组件化
  5. Vue源码解析之 合并配置
  6. Vue源码解析之 生命周期
  7. Vue源码解析之 响应式对象
  8. Vue源码解析之 依赖收集
  9. Vue源码解析之 派发更新
  10. Vue源码解析之 nextTick