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


    前言

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

    在介绍完 reactive 之后还有另几个很重要的响应式API也需要了解下,其中包括 reftoReftoRefsshallowRef。了解这些API可以帮助我们更好地管理和跟踪响应式数据的变化。本文还是会以举例子的形式逐一介绍这些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 state = ref("NY");
      effect(() => {
        app.innerHTML = state.value
      });
      setTimeout(() => {
        state.value = "California";
      }, 1000);
    script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    通过例子可以看到1s之后改变数据视图也跟随变,具体如何实现,我们先从例子中的 ref 函数出发。

    RefImpl

    ref 函数接收一个内部值,然后返回一个具有响应性和可变性的 ref 对象。ref 对象有一个.value属性,该属性指向内部值。

    function ref (target) {
      return createRef(target)
    }
    
    // shallow 是否使用浅层响应式
    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)}
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这里重点看下 ref 函数的实现类 RefImpl 做了什么:

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

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

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

    toRaw

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    toReactive

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

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

    初次执行

    在这里插入图片描述
    在这里插入图片描述

    数据更新

    setTimeout(() => {
      state.value = "California";
    }, 1000);
    
    • 1
    • 2
    • 3

    这里触发更新的过程与之前提到的 reactive 更新执行过程基本一样。都是从 targetMap 的子项 depsMap 中获取对应的 effect 函数执行并直接返回最新的值。需要注意的是这里默认触发的 key 的值为 value

    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 data = reactive({ city: "NY" });
      let state = toRef(data, "city");
      effect(() => {
        app.innerHTML = state.value;
      });
      setTimeout(() => {
        state.value = "California";
      }, 1000);
    script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行之后,同样可以看到1s之后改变数据视图也跟随变。

    ObjectRefImpl

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

    function toRef (target, key: string) {
      return new ObjectRefImpl(target, key)
    }
    
    // _object 源对象
    // _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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ObjectRefImpl 的主要作用是创建一个引用对象,该引用对象可以访问和修改目标对象的特定属性。

    初次执行

    在这里插入图片描述
    在这里插入图片描述

    由于初始的数据已经被处理成了响应式,所以在 effect 函数里初次执行 state.value 时等价于访问 data.city本质上相当于触发了 reactive 函数中 reactiveHandlers 中的 get 拦截器,同时由于 get 里面已经默认执行了 track 所以 get value 函数里只需要将值返回就行不需要经行额外的处理 。

    数据更新

     setTimeout(() => {
      state.value = "California";
    }, 1000);
    
    • 1
    • 2
    • 3

    同理执行时 state.value = "California" 本质上是触发 reactive 函数中 reactiveHandlersset 拦截器。由于 set 里面已经默认执行了 trigger ,所以 set value 函数只需要经行赋值就行了 。之后的更新过程和之前类似这里不在赘述。

    toRefs

    在了解了 toRef 的原理之后 toRefs 的原理也就相对来说很简单了,只需要将源对象进行循环遍历r然后对里面的每一个值调用一遍 toRef 即可。

    function toRefs (target) {
      let result = isArray(target) ? new Array(target.length): {}
      for (let key in target) {
        result[key] = toRef(target, key)
      }
      return result
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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({ city: 'NY' })
      effect(() => {
        app.innerHTML = state.value.city
      });
      setTimeout(() => {
        state.value.city = 'California'
      }, 1000);
    script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

    实现响应式

    function shallowRef (target) {
      return createRef(target, true) // true 浅的响应式
    }
    
    function createRef (rawValue, shallow = false) {
      return new RefImpl(rawValue, shallow)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    需要注意的是:

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

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

    初次执行

    在这里插入图片描述

    数据更新

    setTimeout(() => {
      state.value.city = 'California'
    }, 1000);
    
    • 1
    • 2
    • 3

    直接进行 state.value.city = 'California' 的修改时既不会触发 RefImpl 里面的 set value 函数,也不会触发 reactive 函数中 reactiveHandlersset 拦截器。如果直接设置 state.value = { city: 'California' } 的值则会触发 RefImpl 里面的 set value 函数,也触发 vue 会触发响应。

    总结

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

    • refref 函数用于创建一个包含响应式数据的引用对象,它接受一个基本类型或对象类型的参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会触发响应式更新。ref 函数会对对象类型的参数进行深度响应式转换,即递归地将对象的所有属性都转换为响应式的。
    • toReftoRef 函数用于创建一个指向另一个对象属性的响应式引用,它接受一个对象和一个属性名作为参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会同步地访问或修改原对象的属性,并触发响应式更新。toRef 函数不会对对象类型的属性进行深度响应式转换,即只会转换第一层属性。
    • toRefstoRefs 函数用于将一个响应式对象转换为一个普通对象,该普通对象的每个属性都是指向原对象相应属性的响应式引用。它接受一个响应式对象作为参数,并返回一个普通对象。当访问或修改普通对象的属性时,会同步地访问或修改原对象的属性,并触发响应式更新。toRefs 函数不会对对象类型的属性进行深度响应式转换,即只会转换第一层属性。
    • shallowRefshallowRef 函数用于创建一个浅层的响应式引用,它接受一个基本类型或对象类型的参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会触发响应式更新。shallowRef 函数不会对对象类型的参数进行深度响应式转换,即只会转换第一层属性。
  • 相关阅读:
    Macbooster8免费mac清理垃圾软件功能介绍
    小米云原生文件存储平台化实践:支撑 AI 训练、大模型、容器平台多项业务
    计算机毕业设计SSM电影院购票系统【附源码数据库】
    区块链搭建联盟链及控制台安装
    ReentrantLock 源码
    SQLServer导出数据库字典
    rmq文件清理策略
    1535_TriCore编译器Tasking使用_汇编分区、内置函数以及伪指令
    $nextTick的原理及作用
    Automated defect analysis in electron microscopic images-论文阅读笔记
  • 原文地址:https://blog.csdn.net/AXBNMD/article/details/133919417