Vue源码浅析之响应式系统(一)

515 阅读6分钟

Vue.js 2.X版本中响应式的核心在于,使用了Object.defineProperty来进行依赖的收集与触发。

Object.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。其语法是:

Object.defineProperty(obj, prop, descriptor)

obj是要在其上定义属性的对象。prop是要定义或修改的属性的名称。其中核心的是descriptor中的getsetget是一个给属性提供getter方法,在访问该属性时会触发,set是一个给属性提供的setter方法,在对该属性进行修改的时候触发。

Vue.js 正是利用了gettersetter方法将对象变成了响应式对象。

我们从Vue初始化执行的_init方法开始。

  Vue.prototype._init = function (options?: Object) {
		// ...
    
    // expose real self
    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')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

构建响应式对象

Vue初始化阶段,执行了initState(vm)方法,首先判断data是否存在,如果存在,会执行initData(vm)方法。

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

initData(vm)方法如下:

function initData (vm) {
  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 */)
}

首先进行data类型的判断并返回data的对象结果,接着进行data对象中key值的校验,并执行proxy(vm,_data, key)方法将data数据字段代理到实例vm对象上。最后,执行了observe(data, true /* asRootData */),通过调用该函数,正式开始将data数据对象转变为响应式数据对象。

export function observe (value, asRootData) {
  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
}

如果检测对象自身含有__ob__属性,并且该属性是Observer的实例时,直接将该属性的值作为ob的值,该判断是用于避免重复观测一个数据对象。否则,在满足一定的条件下,去实例化一个Observer对象实例。

Observer构造函数

真正将数据对象转换成响应式数据的是Observer函数。

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

  constructor (value: any) {
   // ...
  }

  walk (obj: Object) {
   // ...
  }

  observeArray (items: Array<any>) {
    // ...
  }
}

Observer类的实例拥有三个实例属性,分别是valuedepvmCount,还有两个实例方法walkobserveArray

__ob__属性

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属性,保存了一个Dep实例对象。它的作用是一个收集依赖的集合。

在初始化了三个实例属性之后,执行了def函数,为该数据对象添加了一个我们前面提到的__ob__属性,该属性的值就是当前的实例对象。

假设有如下数据对象:

const data = {
  a: 1
}

经过def方法之后,data会变成:

const data = {
  a: 1,
  __ob__: {
    value: data,
    dep: dep对象,
    vmCount: 0
  }
}

接下去是对于value类型的判断,先不看value是数组的情况,当value是一个纯对象的时候,执行 this.walk(value)

walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

walk函数的代码也较为简单,获取数据对象所有可枚举的属性后,为每个属性去调用defineReactive方法。

defineReactive函数

defineReactive 的作用就是定义一个响应式的对象,给对象动态添加getter和setter。

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 () {
      // ...
    },
    set: function reactiveSetter (newVal) {
      // ...
    }
  })
}

defineReactive 函数一开始初始化了一个Dep对象实例,接着缓存了obj的属性描述符property,并且对于obj结构复杂的情况下,通过childObj递归调用observe方法,这样使得在放在obj中深层嵌套的属性时,也能够正确触发gettersetter

到这里,通过defineReactive 给数据添加了gettersetter,其中,getter用于收集依赖,setter用于触发依赖更新。

依赖收集与更新通知

依赖收集 get

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
    }

可以观察到get方法中通过闭包引用这一开始初始化的dep实例对象,通过执行dep.depend()将依赖收集到dep集合中,这里的Dep.target实际上就是依赖本身。

Dep

Dep是依赖收集的核心所在。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

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

Dep实际上是对于Watcher的管理,其中静态属性target是全局唯一的Watcher。

Watcher

Dep的作用实际上是对于Watcher的管理,因此有必要先了解Watcher的实现。

首先,我们要找到创建Watcher实例的位置。我们回过头去看Vue初始化执行的_init方法,方法的最后一段为:

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

执行完initState方法,创建了响应式对象之后,可以观察到最后执行了vm.$mount(vm.$options.el),这一步是将组件挂在到指定的元素上。

当我们查找$mount的定义后,发现其实际上是调用了mountComponent函数。在其定义的代码中可以看到Watcher的初始化代码:

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

其中Watcher的第二个参数updateComponent ,其定义如下:

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

该函数最终是调用了vm._update()函数。以上两个函数的作用可以简单理解为:

  • vm._render()调用了render方法返回了VNode
  • vm._update()的作用是把生成的VNode渲染为真正的DOM

到这一步,我们可以找到了Watcher实例化的位置,并且了解了Watcher中第二个参数的作用在于将VNode渲染为真正的DOM。这时候我们来观察Watcherconstructor

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // 省略...
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

代码的最后一段中,除了计算属性之外的实例对象都会调用this.get()方法。

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

首先执行的pushTarget(this)会将当前实例赋值给上文提到的Dep类静态属性target,而这个实例对象正是要被收集的目标。

接着定义了value变量,其值为this.getter函数的返回值,该函数我们通过上述constructor中的定义可以看到,正是实例化Watcher对象的第二个参数。

收集过程

由于触发了this.getter函数,对检测目标求值的过程同时也触发了响应式对象中的get拦截器。我们再回过头看defineReactive 函数:

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
}

此时上述的get方法将被执行,Dep.target属性为Watcher的实例对象。这时候接着执行dep.depend()方法。

depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

这里实际上是触发了WatcheraddDep实例方法:

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

addSub方法如下定义:

addSub (sub: Watcher) {
  this.subs.push(sub)
}

这里做出的逻辑判断在于保证同一个数据不会被添加多次,执行的dep.addSub(this)方法便是把当前的Watch订阅到dep集合中的subs中,由此完成了依赖收集的过程。

触发依赖 set

我们首先来看set函数的定义:

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

首先进行对于新值的判断并赋值,最后执行的childOb = !shallow && observe(newVal)是考虑了如果newVal是一个对象的情况,需要对其变成响应式对象。最后执行dep.notify()进行派发更新,通知所有的订阅者。

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实例的update方法。

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }

在一般组件数据更新的场景,会走到最后一个 queueWatcher(this) 方法,这个方法中引入了队列的概念,是针对Vue每次做通知更新时的优化。它的思路是将这些Watcher先添加到一个队列中,在 nextTick 后执行所有 watcherrun,最后执行它们的回调函数。

run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

首先执行的const value = this.get(),重新求值等价于重新执行渲染函数,最终的结果是重新生成了虚拟DOM并更新了真正的DOM,这样就是完成了重新渲染的过程。

总结

我们最终可以用一张原理图来表示这整个过程:

在初始化阶段执行了_init方法,首先通过initState方法构建了响应式对象,为目标数据分别添加了gettersetter方法。

在实例挂载的阶段,初始化了Watcher实例对象,执行了渲染函数。在渲染VNode时会对目标数据进行访问,由此触发了数据对象的getter,这时依赖被收集。

在目标数据被修改时,触发了setter方法,数据层修改后会触发dep.notify方法派发更新通知,在调用Watcher实例方法update后,将这些Watcher先添加到一个队列中,在 nextTick 后执行所有 watcherrun方法,最后通过借用重新求值方法重新调用渲染函数,完成新的DOM层的更新。