Vue 双向绑定原理

以前写 AngularJS 的时候曾经分析过其双向绑定脏检查机制, 转向 Vue 开发也挺久了但一直没有去研究其源码, 也就前段时间去研究下了其 nextTick 的实现. 今天主要看了 Vue 双向绑定的机制, 感觉 Vue 的代码比起 AngularJS 要容易看很多了, 不过东西也挺多的, 代码质量不错, 写篇文章巩固下.

源码分析

Vue 实现双向绑定有一个非常重要的方法就是 defineReactive, 搞懂这个方法对理解 Vue 的双向绑定帮助非常大. 文章源码篇幅比较多, 但其实代码并不难理解.

defineReactive 大致过程如下:

(注: 结合我看的最新的源码, 图中观察整体 data 应该是用的 observe 方法而不是 defineProperty, 这两个方法区别在于是否需要劫持当前对象自身的 getter, setter.)

defineReactive

源码如下:

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

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

在方法第一行就是实例化一个 Dep, Dep 是一个消息订阅器, 它会收集订阅者, 当数据变更时, 通过消息订阅器可以通知所有的订阅者触发其更新操作.

let childOb = !shallow && observe(val)

observe 方法如下:

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

可以看到 observe 方法其实非常简单, 就是判断如果 val 没有被观察过则实例化一个观察者并返回该观察者. Observe 类如下:

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(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      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], 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])
    }
  }
}

同样非常简单, 需要注意的是, Observe 类实例化时会再次创建一个消息订阅器 Dep.

接着, 如果被观察者是数组, 则对数组原型方法进行劫持. 之所以进行劫持是因为需要除了检测数组的值变化之外, 还需要检测其长度的变化等, 通过进行劫持, Vue 可以检测数组的 push, pop, shift, unshift, splice, sort, reverse 操作. 在这些方法被调用时, 如果有新值产生则会为新值进行观察, 不管有没有新值插入最后都会通过触发其消息订阅器去发送广播从而让其依赖进行更新. 劫持操作定义好后对每一个值进行观察, 调用 observe 方法即可.

如果被观察者是对象, 则对每一项都调用 defineReactive 方法.

这里可能有人不太清楚了, 为什么对象的每一项要调用的是 defineReactive 而数组的每一项调用的是 observe 方法. 我们知道 Vue 的数据检测不能检测对象属性的添加以及数组下标对应值的变化和数组 length 赋值. 数组下标赋值和数组 length 赋值不能被检测的原因就在这里.

那为什么不能做到检测呢? 先说简单的, length 这个属性是无法被检测的, 因为 length 无法被重定义, 我们无法在 length 上添加 getter 和 setter. 那么数组的每一项呢? 首先 Vue 数组是不具备 getter 和 setter 的, 因此也无从拦截, 为什么不设置 getter 和 setter 呢? 先把问题放这, 这里先不说这个, 下篇博客再叙述.

observe 方法之后是拦截 getter 和 setter 操作.

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 检测变化的核心了. Vue 会劫持每个对象属性的 getter 和 setter. 这里的 Dep.target 是什么作用呢? 要理解 Dep.target 我们先看下 Watcher.

Watcher 实现了订阅监听, Vue 模板编译的最后一步是创建 Watcher. Watcher 关联了 View 和 Model, 实现了数据的双向绑定.

watcher

Watcher 的构造函数中有如下几段代码:

if (typeof expOrFn === 'function') {
    this.getter = expOrFn
} else {
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
    this.getter = function () {}
    process.env.NODE_ENV !== 'production' && warn(
        `Failed watching path: "${expOrFn}" ` +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
    )
    }
}
this.value = this.lazy
    ? undefined
    : this.get()

Vue 可以 watch 一个值或者函数. 如果是函数则直接赋值给 getter, 否则会进行路径解析. 关于解析部分有兴趣的可以自己去了解下, 这里不叙述.

最后一行调用了 get 方法, 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 即是把当前的 Dep.target 设置为自己. 接着立即调用了本身的 getter 方法. 这就又回到了我们刚刚拦截 getter 的地方了, 此时检测到 Dep.target 不为空, 因此会进行依赖收集处理. 注意后面的 traverse 操作会进行完整的依赖收集以确保变化检测.

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

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

可以看到, watcherdep 设置为了当前实例的依赖, dep 添加了当前的 watcher 为一个订阅者. 这就是依赖收集.

回到刚刚 defineReactive 中的 setter. 当数据改变时, depnotify 方法被调用, 所有的订阅者(watcher) 讲收都通知并通过执行其更新操作来更新 DOM. 从而实现了变更检测.

总结

observe 方法和 defineReactive 这两个方法可能有人分不太清楚. 首先从接收参数来看, observe 方法有两个参数分别为被观察对象以及是否作为根对象. Vue 组件的 data 调用的是该方法. 而 defineReactive 接受五个参数, 用的多是前三个参数分别为当前对象(obj), 要处理的 key 和 val. defineReactive 会为当前 key 新建一个消息订阅器(dep), 然后调用 observe 方法处理 val, 最后劫持 key 的所有 getter 和 setter.

消息订阅器 Dep 在两个地方被实例化, 分别是 defineReactive 方法以及 Observe 类实例化时. 这两个有什么区别呢? 例如对于下面的 data.

data () {
  return {
    a: {
    },
    b: []
  }
}

我们分析下其被观察的过程:

  • data

    • observe(data)
      • new Observer(data) 转为可观察对象, 创建了消息订阅器 data.dep
      • 遍历 key 调用 defineReactive 方法
  • data.a

    • defineReactive(data, 'a', data.a)
      • 创建了消息订阅器 dep1
      • observe(data.a)
        • new Observer(data.a) 转为可观察对象, 创建了消息订阅器 data.a.dep
        • 遍历 key 调用 defineReactive 方法
      • 劫持 data.a 的 getter 和 setter. 当有依赖 data.a 的订阅者 watcher 出现时, watcher 把自身加入到了 dep1 的订阅者列表中, 同时 dep1 也被加入到 watcher 的依赖中. 接着, 因为 data.a.dep 存在, 也同样进行依赖收集和订阅.
  • data.b

    • defineReactive(data, 'b', data.a)

      • 创建了消息订阅器 dep2

      • observe(data.b)

        • new Observer(data.b) 转为可观察对象, 创建了消息订阅器 data.b.dep

        • argument(data.b)

          劫持数组的原生方法. 例如调用 push 方法时, 将触发 data.b.dep 进行广播

        • 遍历数组每一项调用 observe 方法调用 observe 方法 (observe 方法接收的值如果不是数组或对象则不观察)

      • 劫持 data.b 的 getter 和 setter. 当有依赖 data.b 的订阅者 watcher 出现时, watcher 把自身加入到了 dep2 的订阅者列表中, 同时 dep2也被加入到 watcher 的依赖中. 接着, 因为 data.b.dep 存在, 也同样进行依赖收集和订阅.

      • dependArray. 由于当前 value 是数组, 遍历数组每一项, 如果该项是可观察的对象则对该对象的消息订阅器同样进行依赖收集和订阅, 如果该项是可观察的数组, 则继续遍历数组重复上述操作.

整个过程大概如上, 简单概括就是每个对象和数组都被转为可观察的, 对象的每个 key 的 getter 和 setter 都被劫持(其 value 可能是数组也可能是对象), 当被 watcher 依赖时, watcher 会设置 Dep.target 为自身然后手动调用数据的 getter 来进行订阅和依赖收集处理以实现双向绑定.

可以看到数组比对象多出了 dependArray 这一步, dependArray 操作让子数组的 push, pop 等操作也能被检测到.


参考资料:

  1. Dive into Vue.js
  2. Vue.js 中的 $watch
  3. 剖析Vue原理&实现双向绑定MVVM