vue3 源码学习,实现一个 mini-vue(三):ref 的响应式

1,545 阅读7分钟

前言

本篇文章原文来自 我的个人博客 github地址

在上一章中我们完成了 reactive 函数,同时也知道了 reactive 函数的局限性,知道了只靠 reactive 函数,vue 是没有办法构建出完善的响应式系统的。

所以我们还需要另外一个函数 ref。

本章我们将致力于解决以下三个问题:

  1. ref 函数是如何进实现的?
  2. ref 函数是如何构建简单数据类型的?
  3. 为什么 ref 类型的数据,必须要通过 .value 访问?

1. ref 对于复杂类型数据的响应性

我们知道 ref 其实也是可以实现复杂类型数据的响应性的,那么它是如何实现的呢?我们从下面这段程序开始研究

<div id="app"></div>
<script>
  const { ref, effect } = Vue

  const obj = ref({
    name: '张三'
  })

  // 调用 effect 方法
  effect(() => {
    document.querySelector('#app').innerText = obj.value.name
  })

  setTimeout(() => {
    obj.value.name = '李四'
  }, 2000)
</script>

1.1 源码阅读

  1. 我们直接进入源码 packages/reactivity/src/ref.ts 之下第 80 行,找的 ref 函数的实现,并在这里打下断点。

image.png

  1. 可以看到 ref 函数中最后就是返回了一个 RefImpl 对象,我们进到 RefImpl 类中。

image.png

  1. RefImpl 类的构造函数中 执行了一个 toReactive 的方法,传入了 value 并把返回值赋值给了 this._value,那么我们来看看 toReactive 的作用

image.png

  1. toReactive 方法把数据分成了两种类型:1. 复杂类型调用了 reactive 函数,即把 value 变为响应性的。2.简单数据类型:直接把 value 原样返回。

  2. 而且,RefImpl 类 还 提供了一个分别被 get 和 set 标记的函数 value。1.当执行 xxx.value 时,会触发 get 标记。2.当执行 xxx.value = xxx 时,会触发 set 标记。

至此 ref 函数执行完成。

  1. 接下来开始执行 effect 函数。effect 函数我们在上一章的时候跟踪过它的执行流程。我们知道整个 effect 主要做了 3 件事情:1.生成 ReactiveEffect 实例。2.触发 fn 方法,从而激活 getter。3. 建立了 targetMap 和 activeEffect 之间的联系。
  2. 通过上述可知,在执行 obj.value.name = '张三' 时,会执行 RefImpl 类中的 get value 方法,而 get value 方法中 实际执行的是 trackRefValue,我们直接跳到 trackRefValue 中

image.png 8. 在 trackRefValue 中,触发了 trackEffects 函数,并且在此时为 ref 新增了一个 dep 属性。而 trackEffects 其实我们是有过了解的,我们知道 trackEffects 主要的作用就是:收集所有的依赖

至此 get value 执行完成

  1. 接着,在两秒之后,修改数据源了:obj.value.name = '李四',这里的步骤可以拆分成两步
const value = obj.value
value.name = '李四'
  1. 第一步 const value = obj.value,此时还会触发一遍 get value 中的 trackRefValue 函数。

image.png

  1. 但是这次不一样了, 这次 activeEffect 为 undefined,所以不会执行后续逻辑,直接返回 this._value

  2. 第二步 value.name = '李四', 因为 这里的 value 是 toReactive 转化而来的 proxy 对象,根据 reactive 的执行逻辑可知,此时会触发 trigger 触发依赖。

  3. 至此,视图上的文字改为 李四 ,程序结束

总结:

  1. 对于 ref 函数,会返回 RefImpl 类型的实例
  2. 在该实例中,会根据传入的数据类型进行分开处理
    1. 复杂数据类型:转化为 reactive 返回的 proxy 实例
    2. 简单数据类型:不做处理
  3. 无论我们执行 obj.value.name 还是 obj.value.name = xxx 本质上都是触发了 get value 4,之所以会进行 响应性 是因为 obj.value 是一个 reactive 函数生成的 proxy

1.2 代码实现

  1. 创建 packages/reactivity/src/ref.ts 模块:
import { createDep, Dep } from './dep'
import { activeEffect, trackEffects } from './effect'
import { toReactive } from './reactive'

export interface Ref<T = any> {
  value: T
}

/**
 * ref 函数
 * @param value unknown
 */
export function ref(value?: unknown) {
  return createRef(value, false)
}

/**
 * 创建 RefImpl 实例
 * @param rawValue 原始数据
 * @param shallow boolean 形数据,表示《浅层的响应性(即:只有 .value 是响应性的)》
 * @returns
 */
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T

  public dep?: Dep = undefined

  // 是否为 ref 类型数据的标记
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 如果 __v_isShallow 为 true,则 value 不会被转化为 reactive 数据,即如果当前 value 为复杂数据类型,则会失去响应性。对应官方文档 shallowRef :https://cn.vuejs.org/api/reactivity-advanced.html#shallowref
    this._value = __v_isShallow ? value : toReactive(value)
  }

  /**
   * get语法将对象属性绑定到查询该属性时将被调用的函数。
   * 即:xxx.value 时触发该函数
   */
  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {}
}

/**
 * 为 ref 的 value 进行依赖收集工作
 */
export function trackRefValue(ref) {
  if (activeEffect) {
    trackEffects(ref.dep || (ref.dep = createDep()))
  }
}

/**
 * 指定数据是否为 RefImpl 类型
 */
export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true)
}

  1. 在 packages/reactivity/src/reactive.ts 中,新增 toReactive 方法:
/**
 * 将指定数据变为 reactive 数据
 */
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value as object) : value

  1. 在 packages/shared/src/index.ts 中,新增 isObject 方法:
/**
 * 判断是否为一个数组
 */
export const isArray = Array.isArray

/**
 * 判断是否为一个对象
 */
export const isObject = (val: unknown) =>
  val !== null && typeof val === 'object'
  1. 在 packages/reactivity/src/index.ts 中,导出 ref 函数:

  2. 在 packages/vue/src/index.ts 中,导出 ref 函数::

至此,ref 函数构建完成。

测试

我们可以增加测试案例 packages/vue/examples/reactivity/ref.html 中:

<script>
  const { ref, effect } = Vue

  const obj = ref({
    name: '张三'
  })

  // 调用 effect 方法
  effect(() => {
    document.querySelector('#app').innerText = obj.value.name
  })

  setTimeout(() => {
    obj.value.name = '李四'
  }, 2000)
</script>

可以发现代码测试成功。

2. ref 对于简单数据类型的响应性

我们继续从下面的代码研究 ref 是如何实现简单数据类型的响应性的

<script>
  const { ref, effect } = Vue

  const obj = ref('张三')

  // 调用 effect 方法
  effect(() => {
    document.querySelector('#app').innerText = obj.value
  })

  setTimeout(() => {
    obj.value = '李四'
  }, 2000)
</script>

2.1 源码阅读

ref 函数 整个 ref 初始化的流程和上一小节完全相同,但是有一个不同的地方,需要 特别注意:因为当前不是复杂数据类型,所以在 toReactive 函数中,不会通过 reactive 函数处理 value。所以 this._value 不是 一个 proxy。即:无法监听 setter 和 getter。

effect 函数

整个 effect 函数的流程与上一小节完全相同。

get value()

整个 effect 函数中引起的 get value() 的流程与上一小节完全相同。

大不同:set value()

延迟两秒钟,我们将要执行 obj.value = '李四' 的逻辑。我们知道在复杂数据类型下,这样的操作(obj.value.name = '李四'),其实是触发了 get value 行为。

但是,此时,在 简单数据类型之下,obj.value = '李四' 触发的将是 set value 形式,这里也是 ref 可以监听到简单数据类型响应性的关键。跟踪代码,进入到 set value(newVal):

image.png

由以上代码可知:

  1. 简单数据类型的响应性,不是基于 proxy 或 Object.defineProperty 进行实现的,而是通过:set 语法,将对象属性绑定到查询该属性时将被调用的函数 上,使其触发 xxx.value = '李四' 属性时,其实是调用了 xxx.value('李四') 函数。
  2. 在 value 函数中,触发依赖

总结:

简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。

只是因为 vue 通过了 set value() 的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数,来完成了一个 “类似于” 响应性的结果。

2.2 代码实现

  1. 在 packages/reactivity/src/ref.ts 中,完善 set value 函数:
class RefImpl<T> {
    private _value: T
    private _rawValue: T
    ...
    
    constructor(value: T, public readonly __v_isShallow: boolean) {
    // 原始数据
    this._rawValue = value
  }

  set value(newVal) {
    /**
     * newVal 为新数据
     * this._rawValue 为旧数据(原始数据)
     * 对比两个数据是否发生了变化
     */
    if (hasChanged(newVal, this._rawValue)) {
      // 更新原始数据
      this._rawValue = newVal
      // 更新 .value 的值
      this._value = toReactive(newVal)
      // 触发依赖
      triggerRefValue(this)
    }
  }
  ...
}

...

/**
 * 为 ref 的 value 进行触发依赖工作
 */
export function triggerRefValue(ref) {
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}
  1. 在 packages/shared/src/index.ts 中,新增 hasChanged 方法
/**
 * 对比两个数据是否发生了改变
 */
export const hasChanged = (value: any, oldValue: any): boolean =>
  !Object.is(value, oldValue)

至此,简单数据类型的响应性处理完成。

测试

创建对应测试实例:packages/vue/examples/reactivity/ref-shallow.html

<script>
  const { ref, effect } = Vue

  const obj = ref('张三')

  // 调用 effect 方法
  effect(() => {
    document.querySelector('#app').innerText = obj.value
  })

  setTimeout(() => {
    obj.value = '李四'
  }, 2000)
</script>


测试成功,表示代码完成。

3. 总结

我们现在来回答一下 前言中的三个问题

  1. ref 函数是如何进实现的?

    ref 函数本质上是生成了一个 RefImpl 类型的实例对象,通过 get 和 set 标记处理了 value 函数

  2. ref 是如何构建简单数据类型的?

    ref 通过 get value() 和 set value() 定义了两个属性函数,通过 主动 触发这两个函数(属性调用)的形式来进行 依赖收集 和 触发依赖

  3. 为什么 ref 类型的数据,必须要通过 .value 访问值呢?

    因为 ref 需要处理简单数据类型的响应性,但是对于简单数据类型而言,它无法通过 proxy 建立代理。只能通过 get value() 和 set value() 的方式来处理对依赖的收集和触发,所以我们必须通过 .value 来保证响应性。