当前位置: 首页 > article >正文

vue3 源码解析(2)— ref、toRef、toRefs、shallowRef 响应式的实现

前言

vue3 源码解析(1)— reactive 响应式实现

介绍完 reactive 之后还有另一个很重要的响应式API,其中包括 reftoReftoRefsshallowRef。这些API在vue3中起着至关重要的作用,它们帮助我们更好地管理和跟踪响应式数据的变化。本文还是通过举例子的形式逐一介绍这些API的工作原理。

ref

举个例子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ref</title>
</head>
<body>
<div id="app"></div>
<!--响应式模块的代码-->
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>
  let { ref, effect } = VueReactivity;
  let name = ref("ref");
  effect(() => {
    app.innerHTML = name.value
  });
  setTimeout(() => {
    name.value = "hello";
  }, 1000);
</script>
</body>
</html>

在这里插入图片描述
通过例子可以看到1s之后改变数据视图也跟随变,在 vue3 中是那如何实现这一效果的呢?我们先从例子中的 ref 函数出发。

实现响应式

ref 函数接收一个内部值,然后返回一个具有响应性和可变性的 ref 对象。ref 对象有一个.value属性,该属性指向内部值。换句话说,无论你传递给 ref 函数什么样的值,它都会返回一个新的对象,这个对象有一个 .value 属性,你可以通过这个属性来获取或设置原始值。

function ref (target) {
  return createRef(target)
}

function createRef (rawValue, shallow = false) {
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private readonly _v_isRef = true // 标识 ref
  private _value: T
  private _rawValue: T
  constructor (value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  // 类的属性访问器并进行依赖收集
  get value () {
    track(this, 'get', 'value')
    return this._value
  }

  set value (newVal) {
    const useDirectValue = this.__v_isShallow
    if (hasChanged(newVal, this._value)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      trigger(this, 'set', 'value', newVal)}
  }
}

这里重点看下 RefImpl 这个类的作用是什么:

  1. RefImpl 是一个类,它用来实现 ref 函数的功能。 对象有一个 .value 属性,可以用来获取或设置原始值,并且会触发依赖收集和更新。

  2. RefImpl 的构造函数接收两个参数,value__v_isShallow。value 是要转换为 ref 对象的原始值,__v_isShallow 是一个布尔值,表示是否使用浅层响应式。RefImpl 会将 value 转换为 _rawValue_value 两个属性,_rawValue 是原始值的副本,_value 是原始值的响应式版本。如果 __v_isShallow 为 true,则 _rawValue_value 相同,不会进行深层响应式转换。

  3. RefImpl 还定义了一个属性访问器 value,用来实现 ref 对象的 .value 属性。当读取 value 时,会调用 track 函数进行依赖收集;当设置 value 时,会判断新值和旧值是否有变化,如果有变化,则更新 _rawValue_value,并调用 trigger 函数通知依赖更新。

  4. RefImpl 还有一个私有属性 _v_isRef,用来标识 ref 对象。这样可以在其他地方判断一个对象是否是 ref 对象,并进行相应的处理。

toRaw

const enum ReactiveFlags {
  RAW = '__v_raw'
}
interface Target {
  [ReactiveFlags.RAW]?: any
}

function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

toRaw 函数的作用是返回一个响应式对象的原始对象。这是一个可以用来临时读取而不会产生代理访问/跟踪开销,或者写入而不会触发更改的逃生舱。

toReactive

const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

toReactive 是一个函数,它接受一个值作为参数。如果这个值是一个对象,那么 toReactive 会使用 reactive 函数将这个对象转换为响应式对象。如果这个值不是对象,那么 toReactive 就直接返回这个值。这个函数的主要作用是将一个可能是对象的值转换为响应式对象。当我们设置 ref 对象的 .value 属性时,如果新值是一个对象,那么 toReactive 会确保这个新值是响应式的,从而使得我们可以追踪这个新值的变化。

执行过程

为了更好的理解每个函数是如何执行的,我们可以通过 debugger 来调试一下。

RefImpl 类的实例

在数据更新之前 RefImpl 类的实例对应的数据如下图所示。

在这里插入图片描述

targetMap

effect 函数执行完成之后,此时依赖收集完之后对应的 targetMap 数据如下图所示。

在这里插入图片描述

数据更新时

执行 name.value = "hello" 更新数据时会触发 set ,此时新值和旧值不一样会触发 trigger。触发 trigger 时会从 targetMap 的子项 depsMap 中获取对应的 effect 函数执行并直接返回最新的值。这里的执行过程与之前提到的 reactive 函数执行过程类似,不了解的可以参考之前的文章。

toRef

举个例子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>toRef</title>
</head>
<body>
<div id="app"></div>
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>
  // toRef 将目标对象中的属性值变成 ref
  let { reactive, toRef, effect } = VueReactivity;
  let state = reactive({ name: "toRef" });
  let name = toRef(state, "name");
  effect(() => {
    app.innerHTML = name.value;
  });
  setTimeout(() => {
    name.value = "hello";
  }, 1000);
</script>
</body>
</html>

在这里插入图片描述
同样可以看到1s之后改变数据视图也跟随变。

实现响应式

toRef 函数可以将一个响应式对象的属性转换为一个 ref 对象。这个 ref 对象会与源对象的属性保持同步,也就是说,修改源对象的属性会更新 ref 对象,反之亦然。

function toRef (target, key: string) {
  return new ObjectRefImpl(target, key)
}

class ObjectRefImpl<T extends object, K extends keyof T> {
  private readonly _v_isRef = true // 标识 ref
  constructor (public readonly  _object: T, public readonly  _key: K) {}
  get value () {
    return this._object[this._key]
  }
  set value (newValue) {
    this._object[this._key] = newValue
  }
}

ObjectRefImpl 类有以下几个特点:

  1. 它有一个私有属性 _v_isRef ,用来标识 ref 对象。

  2. 它有两个公共属性 _object_key ,分别表示源对象和属性名。

  3. 它有一个属性访问器 value ,用来实现 ref 对象的 .value 属性。当读取 value 时,会返回源对象的对应属性值;当设置 value 时,会更新源对象的对应属性值。

执行过程

ObjectRefImpl 类的实例

在数据更新之前 ObjectRefImpl 类的实例对应的数据如下图所示。

在这里插入图片描述

targetMap

因为这里的数据已经被处理成了响应式,当访问 name.value 时实质上是触发 reactive 函数中 reactiveHandlersget 拦截器,所以这里不需要手动触发 track 。此时依赖收集完之后对应的 targetMap 数据如下图所示。

在这里插入图片描述

数据更新时

同理当访问 name.value = "hello" 时实质上是触发 reactive 函数中 reactiveHandlersset 拦截器。所以这里也不需要手动触发 trigger ,之后的更新过程和之前类似这里不在赘述。

toRefs

举个例子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>toRefs</title>
</head>
<body>
<div id="app"></div>
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>
  let { reactive, toRefs, effect } = VueReactivity;
  let state = reactive({ name: "toRef" });
  let { name } = toRefs(state);
  effect(() => {
    app.innerHTML = name.value;
  });
  setTimeout(() => {
    name.value = "hello";
  }, 1000);
</script>
</body>
</html>

上述代码的实现效果和上面的一致,都是在1s之后数据改变视图也改变。

实现响应式

toRefs 函数用于将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是一个 ref 对象,与源对象的对应属性保持同步。这样做的好处是,你可以在不丢失响应性的情况下,将响应式对象的属性解构到各个变量中。

function toRefs (target) {
  let result = isArray(target) ? new Array(target.length): {}
  for (let key in target) {
    result[key] = toRef(target, key)
  }
  return result
}

toRefs 函数做了以下几件事:

  1. 首先,它创建了一个新的空对象或数组 result ,用于存放转换后的 ref 对象。

  2. 然后,遍历 target 的每个属性。对于每个属性,它调用 toRef(target, key) 来创建一个 ref 对象,并将这个 ref 对象存放到 result 的对应属性中。这样,result[key] 就成为了一个与 target[key] 保持同步的 ref 对象。

  3. 最后,它返回 result 。这样,你就可以像操作普通对象一样操作 result ,而不用担心丢失响应性。

具体的执行过程可以参考 toRef 这里就不在赘述。

shallowRef

举个例子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>shallowRef</title>
</head>
<body>
<div id="app"></div>
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>
  let { shallowRef, effect } = VueReactivity;
  const state = shallowRef({ count: 1 })
  effect(() => {
    app.innerHTML = state.value.count
  });
  setTimeout(() => {
    state.value.count = 2
  }, 1000);
</script>
</body>
</html>

与之前不同的是1s之后数据改变了但是视图却并没有更新。

实现响应式

function shallowRef (target) {
  return createRef(target, true)
}

function createRef (rawValue, shallow = false) {
  return new RefImpl(rawValue, shallow) // 浅的
}

需要注意的是:

  1. 与之前创建 ref 函数不一样的是这个函数的第二个参数 true 表示创建的引用是浅的。这意味着,如果你更改了 target 的属性,vue 不会触发任何副作用或计算属性的重新计算。

  2. 如果你省略这个参数或将其设置为 false,那么 createRef 将创建一个深度引用,即 target 的所有属性都将被转换为响应式的。

  3. 如果你直接更改了 target(例如,将其设置为一个新的对象 state.value = { count: 2 }),vue 会触发响应。

执行过程

RefImpl 类的实例

在数据更新之前 RefImpl 类的实例对应的数据如下图所示。

在这里插入图片描述

数据更新时

需要注意的是执行 state.value.count = 2" 更新数据时触发的依然是 get 函数,直接返回原理的值。同时也不会触发 set函数和 effect 函数,所以这里的视图是不会进行更新的。

总结

这篇文章主要介绍了 vue 3 的响应式原理,其中涉及到了 reftoReftoRefsshallowRef 等函数的实现。下面是这些函数的响应式实现的总结:

  • refref 函数用于创建一个包含响应式数据的引用对象,它接受一个基本类型或对象类型的参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会触发响应式更新。ref 函数会对对象类型的参数进行深度响应式转换,即递归地将对象的所有属性都转换为响应式的。
  • toReftoRef 函数用于创建一个指向另一个对象属性的响应式引用,它接受一个对象和一个属性名作为参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会同步地访问或修改原对象的属性,并触发响应式更新。toRef 函数不会对对象类型的属性进行深度响应式转换,即只会转换第一层属性。
  • toRefstoRefs 函数用于将一个响应式对象转换为一个普通对象,该普通对象的每个属性都是指向原对象相应属性的响应式引用。它接受一个响应式对象作为参数,并返回一个普通对象。当访问或修改普通对象的属性时,会同步地访问或修改原对象的属性,并触发响应式更新。toRefs 函数不会对对象类型的属性进行深度响应式转换,即只会转换第一层属性。
  • shallowRefshallowRef 函数用于创建一个浅层的响应式引用,它接受一个基本类型或对象类型的参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会触发响应式更新。shallowRef 函数不会对对象类型的参数进行深度响应式转换,即只会转换第一层属性。

http://www.kler.cn/a/107663.html

相关文章:

  • 城市轨道交通数据可视化的应用与优势
  • React Hooks在现代前端开发中的应用
  • Centos安装Elasticsearch教程
  • Llama微调测试记录
  • Mysql数据库里的SSH连接
  • 1111111111待修改--大流量分析(三)-BUUCTF
  • 【Linux】虚拟机安装Linux、客户端工具,MobaXterm的使用,Linux常用命令
  • redis archive github
  • 数据结构之队列
  • 消息队列中间件面试笔记总结RabbitMQ,Kafka,RocketMQ
  • el-table(vue2中)滚动条被固定列盖住
  • 为什么axios会有params和data两个参数
  • 数字孪生智慧工厂三维可视化系统解决方案,打造新一代智慧工厂
  • 【JAVA学习笔记】48 - 八大常用Wrapper类(包装类)
  • TypeScript - 枚举类型 -字符型枚举
  • ETL工具Kettle
  • 深入浅出排序算法之堆排序
  • SQL server 代理服务启动和查看
  • ArcEngine二次开发实用函数16:获取GDB中的所有图层的名称
  • rust 创建多线程web server
  • 子集生成算法:给定一个集合,枚举所有可能的子集
  • 使用docker-compose私有化部署 GitLab
  • 5G与医疗:开启医疗技术的新篇章
  • freeRTOS学习day4-中断使用消息队列
  • 软考系列(系统架构师)- 2012年系统架构师软考案例分析考点
  • Hive常用DDL操作