探索Vue 3 中的ref函数:从原始值到响应式对象的转变

624 阅读7分钟

前言

此前,我们已经讲述过reactive函数了,现在让我来讲解一下如何自己打造一个与它功能相似的ref函数。首先我们要知道,在 Vue 3 中,ref 是一个函数,用于创建一个响应式对象,将普通的 JavaScript 数据转换为具有响应式特性的数据。

ref.js

首先我们就需要创建一个主要的js文件,以此实现 ref 函数的基础功能。

import { reactive } from './reactive.js'
import { track, trigger } from './effect.js'


export function ref(val) {  // 将原始类型数据变成响应式 引用类型也可以
  return createRef(val)
}

function createRef(val) {
  // 判断val是否已经是响应式
  if (val.__v_isRef) {
    return val
  }

  // 将val变为响应式
  return new RefImpl(val)
}


 // const age = ref({n: 18})
class RefImpl {
  constructor(val) {
    this.__v_isRef = true // 给每一个被ref操作过的属性值都添加标记
    this._value = convert(val)
  }

  get value() {
    // 为this对象做依赖收集
    track(this, 'value')
    return this._value
  }

  set value(newVal) {
    // console.log(newVal);

    if (newVal !== this._value) {
      this._value = convert(newVal)
      trigger(this, 'value') // 触发掉 'value' 上的所有副作用函数
    }
  }

}

function convert(val) {
  if (typeof val !== 'object' || val === null) {  // 不是对象
    return val
  } else {
    return reactive(val)
  }
}

以上代码就是一个简化版本的 ref 函数的实现,主要用于将原始类型数据或引用类型数据转换为具有响应式特性的对象。让我们逐步解释代码的各个部分:

  • ref 函数:

    • ref 函数接受一个参数 val,即要转换为响应式对象的原始数据或引用类型数据。
    • 内部调用 createRef 函数来创建并返回一个响应式对象。
  • createRef 函数:

    • createRef 函数接受一个参数 val,即要转换为响应式对象的原始数据或引用类型数据。
    • 首先检查 val 是否已经是响应式对象,如果是,则直接返回 val
    • 如果不是响应式对象,则调用 RefImpl 构造函数来创建一个新的响应式对象,并返回。
  • RefImpl 类:

    • RefImpl 类用于创建具有响应式特性的对象。
    • 在构造函数中,为每个被 ref 操作过的属性值添加了 __v_isRef 标记,用于标识该属性已经被 ref 转换为响应式对象。
    • 通过 _value 属性来存储值,并为其添加了 getter 和 setter 方法,用于获取和设置值。
    • 在 getter 方法中,通过 track 函数进行依赖收集,确保在属性值发生变化时能够触发更新。
    • 在 setter 方法中,如果新值与旧值不同,则更新 _value,并通过 trigger 函数触发对应的副作用函数。
  • convert 函数:

    • convert 函数用于将原始数据转换为具有响应式特性的对象。
    • 如果 val 是原始类型数据或 null,则直接返回 val
    • 如果 val 是对象类型,则调用 reactive 函数将其转换为响应式对象。

effect.js

同样的,在ref函数中,我们也需要进行副作用函数的收集,这个功能的实现方法此前在(手动打造Vue中的reactive函数:探秘数据变化的魔法 - 掘金 (juejin.cn))中我们就已经讲解过了,这里就不过多赘述了,直接贴代码。

const targetMap = new WeakMap();
let activeEffect = null; //得是一个副作用函数

export function effect(fn,options={}) { //watch,computed的核心逻辑
  const effectFn = () => {
    try {
      activeEffect = effectFn
      return fn()
    } finally {
      activeEffect = null
    }

  }
  if(!options.lazy){
    effectFn()
  }
  return effectFn
}

// 为某个属性添加effect
export function track(target,key) {
  // targetMap = { //存成这样的结构
  //   target: {
  //     key: [effect1,effect2,...]
  //   }
  // }

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let dep = depsMap.get(key)
  if (!dep) { //改属性未添加过effect
    dep = new Set()
  }
  if (!dep.has(activeEffect) && activeEffect) {
    // 存入一个effect函数
    dep.add(activeEffect)
  }
  depsMap.set(key, dep)

}

// 触发属性effect
export function trigger(target,key) {
  const depsMap = targetMap.get(target)
  if(!depsMap){ //当前对象中所有的key都没有副作用函数,从来都没有被使用过
    return
  }
  const deps = depsMap.get(key)
  if (!deps) { //这个属性没有依赖
    return
  }

  deps.forEach(effectFn => {
    effectFn() //将该属性上的所有副作用函数全部触发
  });
}

需要了解的朋友可以去了解一下之前的文章。

效果测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script type="module">
        import {ref} from './ref.js'
        import {effect} from './effect.js'
        const age = ref(18)
        age.value = 19
        effect(() => {
            console.log(age.value);
        })
        setInterval(() => {
            age.value = age.value + 1
        }, 1000)
    </script>
</body>
</html>

在以上代码中,我们通过导入自己创建的 ref 函数和 effect 函数,演示了如何创建响应式数据,并且通过副作用函数监听数据的变化。

image.png

结语

至此,我们已经简单的实现了ref函数和reactive函数。在此过程中我们可以发现,ref 函数和 reactive 函数虽然都用于创建响应式数据,但它们有一些关键的区别:

  1. 返回值类型:

    • ref 函数返回的是一个带有 .value 属性的包装对象,而这个 .value 属性才是响应式的。因此,通过 ref 创建的数据需要通过 .value 属性来访问和修改。
    • reactive 函数返回的是一个普通的 JavaScript 对象,但对象内部的所有属性都会被递归地转换成响应式的。
  2. 使用场景:

    • ref 主要用于包装基本类型数据或单个对象,例如数字、字符串、布尔值等,以及一些需要被直接访问和修改的数据。
    • reactive 更适合用于创建包含多个属性的复杂对象,例如包含多个属性的对象或嵌套对象,这样可以一次性地将整个对象及其属性都转换为响应式的。
  3. 访问和修改方式:

    • 使用 ref 创建的数据,需要通过 .value 属性来访问和修改数据的值。
    • 使用 reactive 创建的数据,可以直接访问和修改对象的属性,不需要额外的 .value 属性。
  4. 性能影响:

    • 由于 ref 创建的是一个简单的包装对象,它本身的引用不会发生变化,因此在比较引用时可以使用 === 运算符进行快速的比较。
    • reactive 创建的是一个代理对象,内部存在多个引用,因此比较引用时需要逐层比较对象内部的属性,性能可能略逊于 ref

而为什么如今都是推荐使用ref而不是reactive呢?这是因为:

1、reactive 有一些局限性:

  • 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型。它不能持有如 stringnumber 或 boolean 这样的原始类型。
  • 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失。
  • 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接。

2、主要原因

  1. 更符合单一职责原则:

    • ref 主要用于包装基本类型数据或单个对象,它的职责更加单一明确,只负责包装数据并提供 .value 属性来访问和修改数据。而 reactive 则适用于创建包含多个属性的复杂对象,它需要负责递归地将对象及其属性转换为响应式的,这样可能会与单一职责原则不够符合。
  2. 更容易进行类型推断:

    • 使用 ref 创建的数据是一个带有 .value 属性的包装对象,这样在 TypeScript 中可以更容易进行类型推断,使得代码更加具有类型安全性。相比之下,直接使用 reactive 创建的数据对象可能会在类型推断上稍显复杂。
  3. 更好的性能表现:

    • 由于 ref 创建的数据是一个简单的包装对象,它本身的引用不会发生变化,因此在比较引用时可以使用 === 运算符进行快速的比较,这样可以提升一定的性能。相比之下,reactive 创建的是一个代理对象,内部存在多个引用,比较引用时需要逐层比较对象内部的属性,性能可能略逊于 ref

综上所述,虽然 refreactive 在功能上都能实现响应式数据的创建,但是推荐使用 ref 主要是因为它在语法上更加简洁明了,符合单一职责原则,更容易进行类型推断,同时也具有更好的性能表现。