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

相关文章
|
6天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
3天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
16 7
|
5天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
17 3
|
3天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
19 1
|
3天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
20 1
|
6天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
6天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
10天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
17 0
|
7天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
7天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
27 9