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

lxf2023-05-04 22:20:01

前言

本篇文章原文来自 我的个人博客 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 函数的实现,并在这里打下断点。

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

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

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

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

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

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

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

至此 ref 函数执行完成。

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

vue3 源码学习,实现一个 mini-vue(三):ref 的响应式 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 函数。

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

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

  2. 第二步 value.name = '李四', 因为 这里的 valuetoReactive 转化而来的 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。即:无法监听 settergetter

effect 函数

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

get value()

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

大不同:set value()

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

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

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

由以上代码可知:

  1. 简单数据类型的响应性,不是基于 proxyObject.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 类型的实例对象,通过 getset 标记处理了 value 函数

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

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

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

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