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>reftitle>
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>

vue3 源码解析(2)— ref、toRef、toRefs、shallowRef 响应式的实现_第1张图片
通过例子可以看到1s之后改变数据视图也跟随变,在 vue3 中是那如何实现这一效果的呢?我们先从例子中的 ref 函数出发。

实现响应式

这里先简单看下核心函数 ref 代码实现。

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 会确保这个新值是响应式的,从而使得我们可以追踪这个新值的变化。

执行过程

ref 所涉及的代码已基本编写完成,为了更好的理解每个函数是如何执行的,我们可以通过 debugger 来调试一下。

RefImpl 类的实例

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

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

targetMap

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

vue3 源码解析(2)— ref、toRef、toRefs、shallowRef 响应式的实现_第3张图片

数据更新时

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

toRef

举个例子

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>toReftitle>
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之后改变数据视图也跟随变。

实现响应式

例子中涉及到的 reactive 之前的文章也已经提到,这里主要看下核心函数 toRef 代码实现。

toRef

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 类的实例对应的数据如下图所示。

vue3 源码解析(2)— ref、toRef、toRefs、shallowRef 响应式的实现_第4张图片

targetMap

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

vue3 源码解析(2)— ref、toRef、toRefs、shallowRef 响应式的实现_第5张图片

数据更新时

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

toRefs

举个例子

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>toRefstitle>
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之后数据改变视图也改变。

实现响应式

有了对 toRef 的理解那么 toRefs 理解起来也就简单了很多。

toRefs

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>shallowReftitle>
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之后数据改变了但是视图却并没有更新。

实现响应式

shallowRef

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 类的实例对应的数据如下图所示。

vue3 源码解析(2)— ref、toRef、toRefs、shallowRef 响应式的实现_第6张图片

数据更新时

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

总结

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

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

你可能感兴趣的:(javascript,前端,源码解析,vue3)