源码层面解读Vue响应式原理

在 Vue(以下均指的是 2.x 版本) 中,数据模型仅仅是普通的 JavaScript 对象,当你修改它们时,视图会进行更新,这就是 Vue 的响应式系统,其设计模式就是观察者模式。

思路梳理

以下为阅读源码时,对响应式实现原理的理解,为了思路更清晰,仅考虑最简单的情况,并且可能会省略或修改部分代码。

首先,顺着 vue 的生命周期开始:

beforeCreate 与 created

首先 在 _init 方法中执行 beforeCreate 与 created :

Vue.prototype._init = function (options?: Object) {
  // ...
  callHook(vm, 'beforeCreate')
  // ...
  initState(vm)
  // ...
  callHook(vm, 'created')
  // ...
}

记住这里执行了一个方法: initState

#####initState

export function initState (vm: Component) {
	// 略:...初始化 props,methods 等
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 略:...初始化 computed,watch 等
}

这里的 initData 代码如下:

function initData (vm: Component) {
  // 略:...将 data 的每个属性都挂在 vm 上,这样可以通过 this.xx 直接访问到
  // observe data
  observe(data, true /* asRootData */)
}

记住 observe 这个方法。

observe

observe 用于创建一个 observer 实例:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  // 判断是否已经 observe 过,是的话直接返回
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // observer 最终会挂在 value 的 __ob__ 属性上,因此可以这么判断是否  observe 过,详情看 Observer 的代码
    ob = value.__ob__
  } else if (...) {
    // 否则满足一系列条件的情况下就会重新实例化一个 observer
    ob = new Observer(value)
  }
  return ob
}

实例化 observer 的过程中实际上会将传入对象的每个属性通过 defineReactive 方法转成响应式:

export class Observer {
  constructor (value: any) {
    this.value = value
    // 初始化数据模型的 dep 实例
    this.dep = new Dep()
    // observer 最终会挂在 value 的 __ob__ 属性上
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 略:...重写 value 的一些数组方法
      // 略:...遍历数组元素并 observe
    } else {
      this.walk(value)
    }
  }
  
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
       // 遍历传入的每个属性,并用 defineReactive 转成响应式
      defineReactive(obj, keys[i])
    }
  }
}
defineReactive

上面代码中最核心的一个函数是 defineReactive,做了以下几件事情:

  1. 初始化目标属性的 dep 实例,这实际上是一个发布者
  2. 对该属性的子对象递归调用 observe
  3. 通过Object.defineProperty劫持该属性的 getter 与 setter:
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
) {
  // 初始化目标属性的 dep 实例
  const dep = new Dep()
	// 略:... 一些容错判断
  // 略:... 一些初始化
  
  // 递归 observe
  let childOb = !shallow && observe(val)
  // 劫持 get 与 set
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...一旦获取该属性就会进行依赖收集
    },
    set: function reactiveSetter (newVal) {
      // ...一旦设置了新值就会派发更新
    }
  })
}

这里就用到了观察者模式:当这个属性更新时,会通过发布者通知到观察者,这些观察者收到通知后更新视图。这里发布者实际上是Dep 实例,观察者实际上是 Watcher 实例,它的 update 方法可以更新视图。

所以,依赖收集实际上就是将Wathcer 实例添加到 Dep 实例的观察者列表。

reactiveGetter 和 reactiveSetter 这两个方法的具体实现我们先放一边,这里只需要知道大概的作用即可,后面会详细介绍。我们现在来看看什么时候会触发 get 呢?答案是在挂载组件的时候。

beforeMount 与 mounted

然后是在 mountComponent 中执行 beforeMount 以及 mounted:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...
  callHook(vm, 'beforeMount')
  // 略:...这里有个非生产环境的判断,这里不考虑简化成如下:
  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
   }
  // 这里实例化 Watcher:可以看作是更新视图用的观察者,这里省略一些参数
  new Watcher(vm, updateComponent)
  // ...仅考虑 new Vue 的情况,代码简化如下
  callHook(vm, 'mounted')
  return vm
}

挂载组件的时候就会实例化一个 Watcher

Watcher

当我们实例化 Watcher 的时候:

let uid = 0

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
  ) {
    // 略:...一些初始化
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    // 略:...一些初始化
    // 略:...对 this.getter 的一些初始化
    if (this.computed) {
      // computed 的情况,本文暂不考虑
    } else {
      // 初始化时就会执行 get 方法
      this.value = this.get()
    }
  }

  get () {
    // pushTarget 用于将 watcher 实例挂到 Dep 的 target 这个静态属性上
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 此时的 this.getter 实际上就是 updateComponent
      value = this.getter.call(vm, vm)
    } catch (e) {
      // 略:...错误处理
    } finally {
      // 略:...递归访问 value,触发它所有子项的 getter
      this.cleanupDeps()
    }
    return value
  }
  // ...
}

pushTargetwatcher 实例挂到 Deptarget 这个静态属性, 用于 Dep 内部可以访问到:

function pushTarget (_target: ?Watcher) {
	// ...
  Dep.target = _target
}

接着调用 this.getter, 这里的 this.getter 实际上就是 updateComponent:

vm._update(vm._render(), hydrating)

vm._render() 会生成 VNode,这个过程会访问 vm 上的数据,至此就会触发数据对象的 getter,进行依赖收集。

思路小结

至此我们先梳理一下这一整个过程,以免遗忘:

beforeCreate => 
	initState -> observe -> new Observer
  -> defineReactive:实例化 Dep,劫持对象属性 getter 与 setter
=> created
=> beforeMount
  实例化 Watcher -> 将 watcher 实例绑定到 Dep.target,并触发对象属性的 getter,收集依赖
=> mounted
=> 当数据模型变更时,触发 setter,更新视图

在 defineReactive 中有个关键的步骤是实例化 Dep

Dep

上面说过 Dep 实例是一个发布者,其代码其实很简单:

export default class Dep {
  constructor () {
    this.subs = []
  }
  
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

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

  depend () {
    if (Dep.target) {
      // 这里将 Dep.target 即 watcher 添加到 subs 里
      Dep.target.addDep(this)
    }
  }

  notify () {
    // 发布通知
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

我们最终调用的是 depend方法来添加观察者,即Dep.target.addDep,我们知道此时 Dep.target 实际上就是 watcher 实例。

// ...
addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    // 将发布者 id 添加到 this.newDepIds
    this.newDepIds.add(id)
    // 将发布者 添加到 this.newDeps
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      // 如果旧的 发布者id 列表中没有这个发布者 id,那么就添加到发布者的观察者列表中
      dep.addSub(this)
    }
  }
}
// ...

而在 watcher 中,最终还是调用了传入的 dep 实例的 addSub 来添加到 dep 的观察者列表中。之所以这么设计,其原因是为了能在 watcher 中维护一个自己订阅的发布者列表。

那么 watcher 中的发布者列表有什么用呢?还记得 Watcher 实例化的最后会调用 this.cleanupDeps() 吗?

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}

这个函数实际上的作用是:

  1. 如果无需再观察,则从发布者的观察者列表中移除

  2. 将本次的发布者 id 列表记录在 this.depsIds 中,清空 this.newDepIds

  3. 将本次的发布者列表记录在 this.deps 中,清空 this.newDeps

    那么具体是如何将对象属性转为响应式的呢,我们看看其具体实现:

reactiveGetter
function reactiveGetter () {
  // ...
  // 此时的 Dep.target 就是 watcher,关于 Dep 后面会详细解释
  if (Dep.target) {
    // 这里会将 watcher 实例添加进 dep 的 subs 中维护,即收集依赖
    dep.depend()
    if (childOb) {
      // 子对象的依赖收集
      childOb.dep.depend()
      // 略:...对数组的一些处理
    }
  }
  // 返回属性值
  return value
}
reactiveSetter
function reactiveSetter (newVal) {
  // value:属性值
  const value = ...
  // 略:...一些判断是否触发 setter 的条件
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  // 设置新值后需要重新 observe
  childOb = !shallow && observe(newVal)
  // 发布通知
  dep.notify()
}

总结

本文简化了一些细节,但是 Vue 的响应式原理大致如此。当理解了原理之后,如果后面遗忘了的话,以下为重要的记忆点:

  1. observe:实例化 Observe,参数为数据模型,实例化过程会调用遍历对象属性调用 defineReactive
  2. defineReactive:实例化 Dep,劫持对象属性的 getter 与 setter,getter 时进行依赖收集
  3. 在挂载组件的时候,会实例化 Watcher,此时会触发 getter
  4. 当更新对象属性时,会调用 Dep 实例的 notify,触发 watcher 的 update