Vue 数据响应式原理

Vue 数据响应式原理

Vue2 中的数据响应式原理用的是观察者模式,下面用一张流程图来简单说明一下观察者模式的原理。

在这里插入图片描述

从流程图不难看出,观察者模式的核心就是 Dep 和 Watcher 这两个对象。Dep 负责收集依赖(这里的依赖实际就是 watcher),并在监听到数据变化的时候发送通知。发送通知的过程实际就是调用 watcher.update() 方法。从而更新视图内容。

下面我们来详细看看 Vue 具体是如何实现数据响应式的。

我将 Vue 实现数据响应式的过程分为三步:

  • 创建响应式数据,也就是创建 Observer 对象(这里会创建 dep 对象)
  • 收集依赖和发送通知的过程
  • 创建依赖 Watcher

创建响应式数据

Dep 对象是收集依赖的容器。没有容器又如何能装下依赖,所以在 Vue 实例化的时候都会对数据进行初始化,如 props,data 等数据都是要进行响应式处理的。因此,首先我们要知道 Vue 如何将这些普通的数据转换为响应式属性。

首先我们要知道 Vue 内有那些数据是响应式的,这里重点分析 data 属性的初始化。vue 处理 data 属性是在 Vue 实例初始化函数 _init 中进行的

_init 方法定义的地方是在:src/core/instance/init

在这里插入图片描述

而初始化 data 的过程是在上图的 initState 方法中,该方法是负责初始化我们的一些状态成员

initState 方法定义在:`src/core/instance/state

在这里插入图片描述

标红的地方就是初始化 data 的函数。

初始化 data 的入口我们已经找到了,由于代码比较长,这里我按自己的理解来说说它是如何初始化 data 的。

这里我们假设 data 的值为

data: {
  name: 'vue',
  age: 3
}

基于这个data 的值看看,Vue 是如何处理的

initData

在处理 data 响应式前,会对 data 内的参数进行检查,避免 data 内的 命名与 props 或者 methods 内的属性名重复,这里不详细讨论。

在 initData 函数最后有这样一段代码

在这里插入图片描述

将 data 传入到 observe函数进行处理,observe 函数是对某个对象进行响应式处理的一个函数

observe

路径:src/core/observer/index

observe 内的逻辑也相对比较简单

源码

function observe(value: any, asRootData: ?boolean): Observer | void {
  // 判断传入的值是否是对象,不是直接返回空
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 判断传入的对象是否存在 __ob__ 属性,__ob__属性实际就是用于存储 observe
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 存在,则将 __ob__ 值赋值给 ob
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 不存在则 new 一个 observer 对象 并赋值给 ob
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  // 最后返回 ob
  return ob
}

可以看到 observe 方法实际就是给一个对象添加一个值为 observer 对象的 __ob__ 属性,所以接下来要看看 Observer 实例初始化做了什么事情

Observer 类

路径:src/core/observer/index

来看看类的 constructor 做了什么

{
  ...
  constructor(value: any) {
    this.value = value
    // 新建一个 dep 对象用于收集依赖
    this.dep = new Dep()
    this.vmCount = 0
    // 给监听的对象新增一个 __ob__ 属性,ob 就代表当前的 observer
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 如果需要观察的值是数组,则需要对该值的 proto 进行处理
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // 通过 walk 函数处理需要监听的对象内部的属性
      this.walk(value)
    }
  }
}

传入的值是 data,data 是

data: {
  name: 'vue',
  age: 3
}

所以根据上面的代码,最终会走到 this.walk(value)(value 的值就是 data)

{
  // walk 源码
  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 通过 defineReactive 来使内部的属性是响应式的
      defineReactive(obj, keys[i])
    }
  }
}

walk 是observe 的一个原型方法,它主要就是遍历对象内的属性,并通过 defineReactive 来将这些属性设置为响应式的

defineReactive

路径:src/core/observer/index

由于其内部代码较多,这里主要说说其核心步骤:

  • 实例化 dep 对象为该属性收集依赖
  • 尝试对该属性的值进行 observe
  • 缓存初始的 getter/setter 方法,如果存在,这是为了防止用户自己定义过属性的 getter/setter
  • 通过 Object.definProperty 重新该属性的描述符,这里比较重要的是 getter/setter

这里总结一下,我认为创建响应式数据最核心的函数是 defineReactive,在 defineReactive 中我认为最重要的是两件事情,收集依赖和发送通知。

下面来看看收集依赖的过程

收集依赖和发送通知的过程

收集依赖

收集依赖的过程实际是在 defineReative 中重新定义 getter 的过程中

先来看看 getter 内部做了什么

{
  get: function reactiveGetter() {
    // 获取属性对应的值,并调用初始的 getter
    const value = getter ? getter.call(obj) : val
    // 若 Dep.target 存在(target 一般指的是依赖),就会尝试为该属性收集依赖
    if (Dep.target) {
      // 当该属性被获取的时候,使用 dep.depend() 收集依赖
      dep.depend()
      if (childOb) {
        // 若该属性的值是一个响应式数据,使用 childOb.dep.depend() 为该对象收集依赖
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  }
}

不难看出,当 get 方法实际就是一个收集依赖的过程,当该属性被获取的时候,它会尝试为该属性收集依赖,等待未来的时候去给这些依赖发送通知更新视图。

dep.depend() 方法就是用于收集依赖,该方法会将 Dep.target (watcher) 添加到 dep.subs 数组中,有兴趣可以看看源码。

在这里插入图片描述

发送通知

发送通知的操作实际就是在修改响应式数据后进行的,而修改值的操作是在 setter 中,所以我们看看setter 内部的原理

{
  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)
// 发送通知
dep.notify()

当响应式属性的值被修改后,会触发 dep.notify() ,调用所有 watcher 的 update 方法来进行更新操作

在这里插入图片描述

创建依赖 Watcher

上面说了收集依赖的过程。那么依赖又是什么时候生成的呢。Watcher 有三种类型,computed watcher、侦听器watcher、渲染watcher。

我们来看看渲染 watcher ,也就是负责渲染实例的watcher。在 mountComponent 中。

渲染 wathcer 的入口

路径:src/core/instance/lifecycle -> mountComponent

在这里插入图片描述

我们先来看看 watcher 的构造函数,它在 src/core/observer/wathcer 文件中

我们看看它前几个参数的含义

在这里插入图片描述

根据构造函数内的形参,我们来看看 上一张图传递了什么参数

- vm:vue 实例
- expOrFn 表达式:updateComponent 函数
- cb 回调:noop 空函数
- options 配置参数:一个含有 before 函数的对象
- isRenderWatcher 是否渲染 watcher:true

渲染 wathcer 初始化

然后我们来看看 它初始化做了什么,由于源码比较长,这里我就按自己理解总结一下:

  • 参数初始化
  • 给 this.getter 赋值,若 expOrFn 是函数就直接赋值给 this.getter,若不是则用 parsePath 函数处理 expOrFn 的结果赋值给 this.getter。
  • 执行 this.get() 并将其返回值赋值给 this.value

所以这里着重看 this.get() 执行的过程

this.get()

{
  get() {
    // pushTarget 会将当前的 watcher 入栈,并记录到 Dep.target 中
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用 this.getter 这一步是最核心的步骤,收集依赖
      // 再调用 this.getter 的时候,会触发用户传入的 函数 或者 字符串表达式,在这个过程中,会触发属性的 get 方法,对于响应式数据,在这一步中会收集所有的依赖
      // 如 渲染 watcher 中,this.getter 就是 updateComponent,该方法用于更新视图
      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)
      }
      // 清理工作,清除栈中的 watcher
      popTarget()
      // 将 watcher 从 dep.subs中移除,以及将 dep 从 watcher.deps 中移除,因为整个 watcher 以及完成,不再需要了
      this.cleanupDeps()
    }
    return value
  }
}

在 this.get() 中有几个比较核心的步骤:

pushTarget()

在上面可以知道,在收集依赖的时候,会将 Dep.target 添加到 dep.subs 中。

那么 Dep.target 的值是什么、又是什么时候赋值的?

Dep.target 实际就是 watcher 对象,而pushTarget(this) 的核心就是将当前的 watcher 赋值给 Dep.target。

pushTarget 实际还会将当前的 wacther 进行入栈的操作,这个主要是为了解决父子组件嵌套的问题。

这一个步骤我称为:确认依赖

this.getter()

当在获取某个响应式属性的时候会触发其 getter,从而触发收集依赖的操作。

那么,它又是在什么时候去触发获取响应式属性这个操作的呢?

其实就是 this.getter()。我们知道 this.getter 实际就是用户传入的函数表达式或者函数,但最后都会转换为函数,当执行 this.getter() 时。在函数内部会触发某些响应式属性的 getter 方法,从而触发收集依赖的操作。并等待在合适的时候向这些依赖发送通知。

并将 this.getter() 的返回值赋值给 value,并在最后返回

这一步骤我成为:触发依赖收集

清理工作

在最后还会进行清理工作,主要是清除栈中的 watcher

总结

确认依赖

this.getter()

当在获取某个响应式属性的时候会触发其 getter,从而触发收集依赖的操作。

那么,它又是在什么时候去触发获取响应式属性这个操作的呢?

其实就是 this.getter()。我们知道 this.getter 实际就是用户传入的函数表达式或者函数,但最后都会转换为函数,当执行 this.getter() 时。在函数内部会触发某些响应式属性的 getter 方法,从而触发收集依赖的操作。并等待在合适的时候向这些依赖发送通知。

并将 this.getter() 的返回值赋值给 value,并在最后返回

这一步骤我成为:触发依赖收集

清理工作

在最后还会进行清理工作,主要是清除栈中的 watcher

总结

当以上三个步骤都做完后,当响应式数据的值发送改变的时候,就会触发其 dep.notify() 方法,从而触发其 dep.subs 数组中所有依赖的 update() 方法,从而进行了更新的操作,这个操作可能时更新视图,也可能是更新某些值。