Vue源码系列之响应式原理

1,517 阅读8分钟

背景

近期发现组内的同学在需求开发中,经常会遇到Vue数据改变但视图没有更新的问题,最后发现主要的问题还是大家对Vue的响应式原理理解的不够深入,为了帮助大家真正的理解Vue响应式,避免在以后的开发过程中出现同样的问题,我们在组内组织了Vue响应式相关的源码分享,下边我将分享的内容整理如下,方便大家日后回顾和理解

什么是响应式

在开始之前,我们首先需要清楚究竟什么是响应式。简单来说,就是当你修改data中的数据时,视图会重新渲染,更新为最新的值。这使得我们的状态管理变的非常的简单,我们只需要关注数据本身,而不用关心数据的渲染,大大的提高了我们的开发效率。但这也时常会给我们带来一些问题,当我们的代码没有得到我们预期的执行结果时,我们常常也会束手无策,不知道如何去排查问题的原因,所以理解其工作原理就变得十分重要

Vue是如何实现响应式的

现在,假设我们自己要实现Vue的响应式,需要解决哪些问题?

  1. Vue如何侦测数据的变更?
  2. Vue如何知道应该通知哪些视图进行更新?
  3. Vue如何通知需要更新的视图?

对于上边的三个问题,Vue都给出了对应的解决方案

  1. 数据劫持
  2. 依赖收集
  3. 依赖更新

数据劫持的核心其实就是Object.defineProperty方法,这个方法的具体使用方式这里我们不做过多解释,想要了解的可以移步此处进行查看,这里我们需要重点关注的是它的get和set,可以对属性的获取和设置操作进行拦截。当然不同于Vue2,Vue3是通过ES6的Proxy进行数据代理的方式进行数据变更侦测的,Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,此外Proxy支持代理数组的变化

关于依赖收集和依赖更新,在Vue中主要依赖三个类来实现,分别是Observer、Dep、Watcher,它们三个一起就组成了一个发布订阅模式(不了解发布订阅模式的可以参考这篇文章

Observer

Observer作为一个发布者,它会将普通对象变成响应式的对象,这样当对象的属性被引用时它会收集依赖,当对象的属性被修改时,它会触发依赖更新。Observer会递归遍历对象的每一个属性,确保对象及子对象的所有属性都变成响应式的

observe方法作为Vue响应式的一个重要的入口方法,其作用就是把参数传入的对象变成响应式对象

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) { // 如果不是对象类型或者属于VNode类型就直接返回
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 判断是否存在__ob__属性,如果存在说明已经是响应式对象了
    ob = value.__ob__
  } else {
    ob = new Observer(value) // 如果不是响应式对象,则创建Observer实例,将其变成响应式对象
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer类会对对象和数组分别进行处理,如果是对象,就遍历对象,调用defineReactive方法对每一个属性进行数据劫持;如果是数组,就遍历数组,调用observe方法对每一个元素进行Observer实例化

export class Observer {
  value: any;
  dep: Dep;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep() // 创建Dep实例用于收集依赖

    def(value, '__ob__', this) // 将当前Observer实例挂载到目标对象的__ob__属性上,代表此对象已经变成了响应式对象
    if (Array.isArray(value)) { // 判断是否为数组
      if (hasProto) { // 判断当前环境是否支持__proto__属性
        protoAugment(value, arrayMethods) // 如果支持,就通过覆盖__proto__的方式重写数组的方法
      } else {
        copyAugment(value, arrayMethods, arrayKeys) // 否则,就通过Object.defineProperty的方式去重新定义数组的方法
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) { // 遍历对象的每一个属性通过defineReactive进行数据劫持
    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])
    }
  }
}

defineReactive方法作为Vue响应式里边最核心的一个方法,主要的作用是劫持对象属性的get和set操作,并在get中收集依赖,在set中通知依赖更新

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep() // 创建Dep实例用于收集依赖

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () { // 拦截get方法,进行依赖收集
      const value = val
      if (Dep.target) { // 判断是否存在监听器Watcher
        dep.depend() // 如果存在则将其添加到dep.subs中去,此处很多同学可能会问为什么不直接调用dep.addSub添加依赖,在下边的Dep源码分析的时候我会再做说明
      }
      return value
    },
    set: function reactiveSetter (newVal) { // 拦截set方法进行依赖更新
      val = newVal
      dep.notify()
    }
  })
}

对于此处的Dep.target容易让人产生困惑,它的值对应的是一个Watcher实例,在Vue实例初始化解析模板的时候会创建当前实例的Watcher,而这个Watcher就会被赋值给Dep.target,因此这个Dep.target代表的就是触发get回调的当前正在被解析的模板所属的Vue实例对应的Watcher,所以它是全局唯一的

Dep

Dep扮演的是调度中心的角色,它用于存储依赖,所以每个属性都会拥有自己的Dep实例,当属性的值发生变化时,会遍历订阅者列表,通知所有的订阅者执行自己的更新操作

export default class Dep {
  static target: ?Watcher; // 全局的Watcher
  id: number;
  subs: Array<Watcher>;  // 用于存储依赖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) { // 如果当前有订阅者Watcher,将该dep放进当前订阅者的deps中,并且将当前的订阅者放入订阅者列表subs中
      Dep.target.addDep(this)
    }
  }

  notify () { // 通知依赖更新
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 获取到每一个Watcher,并调用其update方法进行视图的更新
    }
  }
}

对于上边depend方法,跟踪源码一圈后发现最终还是调用了addSub方法添加了依赖,那为什么要绕这么一圈而不是直接调用addSub呢,其实看过Watcher的源码后就会发现,在Watcher的addDep方法中首先将当前dep添加到了Watcher维护deps列表中,然后才将当前Watcher实例添加到了dep维护的subs列表中,这样其实形成了一个双向依赖的关系,所以可以看出,在Vue中一个发布者会被多个订阅者订阅,同时一个订阅者也会订阅多个发布者

Watcher

Watcher就是我们上边说的订阅者,它的主要作用就是接收Dep调度中心的通知进行视图的更新,所以每一个Vue实例都会拥有一个专属的Watcher。在Vue中,Watcher分为渲染Watcher、计算属性Watcher和侦听器Watcher三种,它们分别会监听data、computed和watch。Watcher的代码相对比较复杂,下边的代码会做相应的简化处理方便大家理解核心原理

export default class Watcher {

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) { // 如果是渲染Watcher,将当前Watcher实例绑定到当前Vue实例的_watcher属性上
      vm._watcher = this
    }
    vm._watchers.push(this) // 将当前Watcher实例加入到Vue实例维护的_watchers列表中
    ...
    this.deps = [] // Watcher实例维护的Dep实例的依赖列表,其实Watcher和Dep是一个双向依赖的关系
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    if (typeof expOrFn === 'function') { // 如果是函数则为渲染Watcher或者侦听器Watcher
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn) // 如果是表达式则为计算属性Watcher
    }
    this.value = this.lazy // lazy为true则说明是计算属性Watcher,不需要立即求值
      ? undefined
      : this.get()
  }

  get () {
    ...
    let value
    const vm = this.vm
    ...
    value = this.getter.call(vm, vm) // 调用回调函数,也就是upcateComponent,触发依赖收集
    ...
    return value
  }

  addDep (dep: Dep) { // 此处实现了Dep和Watcher的双向依赖
    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)
      }
    }
  }

  update () { // Watcher更新视图的方法
    if (this.lazy) { // 计算属性Watcher
      this.dirty = true
    } else if (this.sync) { // sync为true的侦听器Watcher
      this.run()
    } else { // 将当前Watcher实例追加到队列中进行异步更新
      queueWatcher(this)
    }
  }

  run () { // 执行watcher的回调函数cb
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        this.cb.call(this.vm, value, oldValue) // 此处会传入侦听器Watcher回调中的value和oldValue
      }
    }
  }
}

这里有一块比较关键的代码,串联起了整个依赖收集的过程,代码如下

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
    vm._update(vm._render(), hydrating) // 调用渲染函数,生成虚拟dom,
  }

  new Watcher(vm, updateComponent, noop, { // 创建当时Vue实例的watcher,当数据发生变化的时候就会触发updateComponent
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

总结

最终,Vue实例的data对象会形成如下的依赖关系

现在,我们梳理一下整个流程

  1. Vue实例初始化,调用observe方法将data对象转换成响应式对象,同时创建Dep用来收集依赖Watcher
  2. 收集依赖,过程如下:
    1. Vue实例挂载之前会实例化一个渲染watcher,在Watcher构造函数里get方法被执行
    2. 执行this.getter.call(vm, vm),其实就是我们上面的看到的updateComponent方法
    3. 执行vm._render()生成虚拟DOM vnode,这个过程中就会触发响应式对象的getter回调
    4. 在getter中调用Dep.depend()方法收集依赖
  3. 当属性的值被修改时,会触发属性的set方法,然后调用Dep.notify通知所有依赖该属性的Watcher去更新视图