响应式

Vue 初始化

在 Vue 的初始化中,会先对 props 和 data 进行初始化

Vue.prototype._init = function(options?: Object) {
  // ...
  // 初始化 props 和 data
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')

  if (vm.$options.el) {
    // 挂载组件
    vm.$mount(vm.$options.el)
  }
}

接下来看下如何初始化 props 和 data

export function initState(vm: Component) {
  // 初始化 props
  if (opts.props) initProps(vm, opts.props)
  if (opts.data) {
    // 初始化 data
    initData(vm)
  }
}
function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = (vm._props = {})
  // 缓存 key
  const keys = (vm.$options._propKeys = [])
  const isRoot = !vm.$parent
  // 非根组件的 props 不需要观测
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    // 验证 prop
    const value = validateProp(key, propsOptions, propsData, vm)
    // 通过 defineProperty 函数实现双向绑定
    defineReactive(props, key, value)
    // 可以让 vm._props.x 通过 vm.x 访问
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

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 (props && hasOwn(props, key)) {
    } else if (!isReserved(key)) {
      // 可以让 vm._data.x 通过 vm.x 访问
      proxy(vm, `_data`, key)
    }
  }
  // 监听 data
  observe(data, true /* asRootData */)
}
export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 如果 value 不是对象或者使 VNode 类型就返回
  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
}
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
    // 通过 defineProperty 为对象添加 __ob__ 属性,并且配置为不可枚举
    // 这样做的意义是对象遍历时不会遍历到 __ob__ 属性
    def(value, '__ob__', this)
    // 判断类型,不同的类型不同处理
    if (Array.isArray(value)) {
      // 判断数组是否有原型
      // 在该处重写数组的一些方法,因为 Object.defineProperty 函数
      // 对于数组的数据变化支持的不好,这部分内容会在下面讲到
      const augment = hasProto ? protoAugment : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 遍历对象,通过 defineProperty 函数实现双向绑定
  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  // 遍历数组,对每一个元素进行观测
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Object.defineProperty

无论是对象还是数组,需要实现双向绑定的话最终都会执行这个函数,该函数可以监听到 setget 的事件。

Object.defineProperty 中自定义 getset 函数,并在 get 中进行依赖收集,在 set 中派发更新。接下来我们先看如何进行依赖收集。

依赖收集

依赖收集是通过 Dep 来实现的,但是也与 Watcher 息息相关

对于 Watcher 来说,分为两种 Watcher,分别为渲染 Watcher 和用户写的 Watcher。渲染 Watcher 是在初始化中实例化的。

接下来看一下 Watcher 的部分实现

以上就是依赖收集的全过程。核心流程是先对配置中的 props 和 data 中的每一个值调用 Obeject.defineProperty() 来拦截 setget 函数,再在渲染 Watcher 中访问到模板中需要双向绑定的对象的值触发依赖收集。

派发更新

改变对象的数据时,会触发派发更新,调用 Depnotify 函数

以上就是派发更新的全过程。核心流程就是给对象赋值,触发 set 中的派发更新函数。将所有 Watcher 都放入 nextTick 中进行更新,nextTick 回调中执行用户 Watch 的回调函数并且渲染组件。

Object.defineProperty 的缺陷

以上已经分析完了 Vue 的响应式原理,接下来说一点 Object.defineProperty 中的缺陷。

如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作,更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。

对于第一个问题,Vue 提供了一个 API 解决

对于数组而言,Vue 内部重写了以下函数实现派发更新

最后更新于