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

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

前言

在上一章中我们完成了 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 之下,找的 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 类 还 提供了一个分别被 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

image.png

  1. 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. 但是这次不一样了, 这次 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)

image.png

由以上代码可知:

  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 来保证响应性。

相关文章
|
21天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
25天前
|
API
vue3知识点:reactive对比ref
vue3知识点:reactive对比ref
28 3
|
25天前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
27 3
|
25天前
|
JavaScript API
vue3知识点:ref函数
vue3知识点:ref函数
32 2
|
29天前
|
缓存 JavaScript UED
优化Vue的响应式性能
【10月更文挑战第13天】优化 Vue 的响应式性能是一个持续的过程,需要不断地探索和实践,以适应不断变化的应用需求和性能挑战。
31 2
|
25天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
24 0
|
9天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
9天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
9天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
9天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。