Vue3 Composition API 新特性

了解 Vue3 Composition API 新特性

1、ref

ref用于创建一个简单的包装对象,将普通的数据类型(如基本类型、对象、数组等)转换为响应式数据。

1.1、ref是什么

  • ref 可以生成 值类型(基本数据类型和复杂数据类型都行) 的响应式数据;
  • ref 可以用于模板 和reactive;
  • ref 用来创建基础类型的响应式数据,模板默认调用 value 显示数据。方法中通过 .value 来修改值;
  • ref 不仅可以用于响应式 ,还可以用于模板的 DOM 元素。

1.2、举个例子🌰

html 复制代码
<template>
  <div>
    <!-- 在 template 中,vue 做了自动展开,不需要 .value -->
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

// 在 script 中创建 ref,并通过 .value 进行访问
const count = ref(0);

// 也可以用ref创建引用类型数据
const obj = ref({ count: 0 }) // OK

function increment() {
  count.value++;
} 
</script>

1.3、核心代码实现

源码文件 : core\packages\reactivity\src\ref.ts

js 复制代码
// ref.ts 文件93 行 
export function ref(value?: unknown) {
  return createRef(value, false) //1 : 提供 ref函数 , false 是否浅复制
}
// ref.ts文件第 127行  
// 调用 ref 返回一个 创建 的方法 createRef 传入 两个值 
/**
 * @param rawValue ref函数传入的参数
 * @param shallow 是否浅复制
 */
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) { // 是否是ref对象 如果是 则 直接返回
    return rawValue
  }
  return new RefImpl(rawValue, shallow) // 否则 创建 ref 对象 传入 rawValue shallow
}


// ref.ts 文件第 134行 
class RefImpl<T> { // 创建一个 ref的 实现类 
  private _value: T // 创建私有的 _value 变量 
  private _rawValue: T // 创建私有的 _rawValue 变量 

  public dep?: Dep = undefined // 是否 dep 
  public readonly __v_isRef = true // 只读的 属性 是否是 ref 

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 实例被 new时 执行 constructor 保存 传入的值
    this._rawValue = __v_isShallow ? value : toRaw(value) // 是否浅复制 , 如果时 则直接返回 传入的值 否则进行 获取其原始对象
        this._value = __v_isShallow ? value : toReactive(value) // 是否浅复制 是 返回原value 否则 转换成 reactive 对象
  }

  get value() { // 获取值的时候 直接将 constructor 保存的值 返回 
    trackRefValue(this) // 跟踪 ref 的 value
    return this._value  // 获取value 是 返回 _value 对象
  }

  set value(newVal) {// 当 设置值的时候 往下看 
  
  // 是否浅复制 or 值身上是否有 __v_isShallow 标识 or 是否是只读的 标识__v_isReadonly
    const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal); 
    // 如果满足 则 返回新设置的值  , 如果不是 则 取出 新值的原始对象 
    newVal = useDirectValue ? newVal : toRaw(newVal)  // 如果 你一个 浅层对象(普通数据类型) 则 原值返回 否则 判断是否能从 代理对象中 取出源值
    if (hasChanged(newVal, this._rawValue)) { // 判断对象是否发生 变化 变了向下走  
      this._rawValue = newVal // 将最新值 赋给 _rawValue
      this._value = useDirectValue ? newVal : toReactive(newVal) // 判断是否是基本数据类型  如果 是 则 将最新值返回 否则 继续转换 reactive
      triggerRefValue(this, newVal) // 触发 ref 的 value 值进行监听更新
    }
  }
}
// 判断是否 是对象 如果是 则 reactive代理 否则 返回 当前的value
export const toReactive = <T extends unknown>(value: T): T => 
isObject(value) ? reactive(value) : value

总结一下 ref 做了什么

  • 调用ref 将定义的数据传入,返回一个创建 ref 响应式数据的函数,是否需要浅层复制,默认为 false,也就意味着一定会走 转换成 reactive。

  • 调用createRef,判断是否是一个 ref 对象 ,是原值返回否则 ,new 一个实现 ref 类。

  • 创建类的私有变量 ,保存传入的value 和 shallow。

  • 判断是否浅层复制,如果是则返回传入的 value,否则取出 ref 的原始值对象。

  • 获取值的时候将保存的值返回出去。

  • 设置值的时候判断当前属性是否是浅层对象 ,如果是则返回该数据 否则调用 toreactive 转换 reactive 进行操作。

  • 触发更新。

1.4、为什么需要用ref

  • 值类型 (即基本数据类型)无处不在,如果不用 ref 而直接返回值类型,会丢失响应式。
  • 比如在 setup 、 computed 、合成函数 等各种场景中,都有可能返回值类型。
  • Vue 如果不定义 ref ,用户将自己制造 ref ,这样反而会更加混乱。

1.5、为何ref需要.value属性

  • ref 是一个对象 ,这个对象不丢失响应式,且这个对象用 value 来存储值。
  • 因此,通过 .value 属性的 get 和 set 来实现响应式。
  • 只有当用于 模板 和 reactive 时,不需要 .value 来实现响应式,而其他情况则都需要。

2、reactive

reactive 方法根据传入的对象 ,创建返回一个深度响应式对象 。响应式对象 看起来和传入的对象一样,但是,响应式对象属性值改动,不管层级有多深,都会触发响应式。新增和删除属性也会触发响应式。

2.1、reactive的特性

  • 响应式转换:reactive函数将普通JavaScript对象转换为响应式对象。这意味着当响应式对象的属性发生变化时,视图会自动更新。

  • 嵌套响应:如果普通对象中的属性值也是对象,那么这些嵌套对象也会被转换为响应式对象,从而实现嵌套的响应式数据。

  • 延迟响应:reactive函数会在访问响应式对象的属性时进行依赖收集,并在属性变化时触发更新,这种响应是延迟的,只有在需要时才会执行。

  • 自动追踪:响应式对象会自动追踪依赖,即在模板中使用响应式对象的属性时,Vue 3会自动进行依赖收集,使得属性与视图之间建立起响应式关系。

  • 递归响应:reactive函数会递归地将对象的所有属性转换为响应式对象,包括嵌套对象和数组。

  • 高效性能:Vue 3使用Proxy实现响应式,相比Vue 2中的Object.defineProperty,Proxy可以更高效地进行依赖追踪和触发更新,从而提升了响应式系统的性能。

2.2、举个例子🌰

html 复制代码
<template>
  <div>
    <span>姓名:{{ data.name }}</span>
    <span>年龄:{{ data.age }}</span>
    <button @click="change">Change</button>
    
    <!-- 通过解构后直接展示 -->
    <span>姓名:{{ name }}</span>
    <span>年龄:{{ age }}</span>
    <button @click="changeByToRef">ChangeByToRef</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, toRefs } from 'vue';

const data = reactive({ name: '张三', age: 18 });

// 直接修改变量的值
function change() {
  data.name = "李四";
  data.age = 20;
}

// 通过toRefs()批量处理,此时通过toRefs解构并且数据不会失去响应性
const { name, age } = toRefs(data);

// 如果直接这样写,这两个 property 的响应性都会丢失。
// const { name, age} = data;

// 经过了toRef的处理,修改变量的值就需要xxx.value 
function changeByToRef() {
  name.value = "王五";
  age.value = 25;
}
</script>

使用 ref 或者 reactive 等函数创建的响应式数据时不能直接使用 ++ 和 -- 等一元运算符。这是因为一元运算符在背后会调用目标对象的 get 和 set 操作,而 Proxy 对象的特性会导致这种操作无法按预期工作。

js 复制代码
import { ref } from 'vue';
const refData = ref(0);

const changeRefData = () => {
  // 推荐用法
  refData.value += 1;
  
  // 不推荐
  // refData.value ++;
};

2.3、核心代码实现

源码文件 : core\packages\reactivity\src\reactive.ts

js 复制代码
// ref.ts 文件93 行 
export function ref(value?: unknown) {
  return createRef(value, false) //1 : 提供 ref函数 , false 是否浅复制
}
// ref.ts文件第 127行  
// 调用 ref 返回一个 创建 的方法 createRef 传入 两个值 
/**
 * @param rawValue ref函数传入的参数
 * @param shallow 是否浅复制
 */
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) { // 是否是ref对象 如果是 则 直接返回
    return rawValue
  }
  return new RefImpl(rawValue, shallow) // 否则 创建 ref 对象 传入 rawValue shallow
}


// ref.ts 文件第 134行 
class RefImpl<T> { // 创建一个 ref的 实现类 
  private _value: T // 创建私有的 _value 变量 
  private _rawValue: T // 创建私有的 _rawValue 变量 

  public dep?: Dep = undefined // 是否 dep 
  public readonly __v_isRef = true // 只读的 属性 是否是 ref 

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 实例被 new时 执行 constructor 保存 传入的值
    this._rawValue = __v_isShallow ? value : toRaw(value) // 是否浅复制 , 如果时 则直接返回 传入的值 否则进行 获取其原始对象
        this._value = __v_isShallow ? value : toReactive(value) // 是否浅复制 是 返回原value 否则 转换成 reactive 对象
  }

  get value() { // 获取值的时候 直接将 constructor 保存的值 返回 
    trackRefValue(this) // 跟踪 ref 的 value
    return this._value  // 获取value 是 返回 _value 对象
  }

  set value(newVal) {// 当 设置值的时候 往下看 
  
  // 是否浅复制 or 值身上是否有 __v_isShallow 标识 or 是否是只读的 标识__v_isReadonly
    const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal); 
    // 如果满足 则 返回新设置的值  , 如果不是 则 取出 新值的原始对象 
    newVal = useDirectValue ? newVal : toRaw(newVal)  // 如果 你一个 浅层对象(普通数据类型) 则 原值返回 否则 判断是否能从 代理对象中 取出源值
    if (hasChanged(newVal, this._rawValue)) { // 判断对象是否发生 变化 变了向下走  
      this._rawValue = newVal // 将最新值 赋给 _rawValue
      this._value = useDirectValue ? newVal : toReactive(newVal) // 判断是否是基本数据类型  如果 是 则 将最新值返回 否则 继续转换 reactive
      triggerRefValue(this, newVal) // 触发 ref 的 value 值进行监听更新
    }
  }
}
// 判断是否 是对象 如果是 则 reactive代理 否则 返回 当前的value
export const toReactive = <T extends unknown>(value: T): T => 
isObject(value) ? reactive(value) : value

总结一下 reactive 做了什么

  1. 调用 reactive 方法传数据,判断对象是否是只读对象,如果是直接返回"无法操作",否则向下继续执行。
  2. 调用 createReactiveObject 返回一个代理对象。
  3. 判断传入的值是不是一个对象,如果不是则直接原路返回,否则判断 target 是不是一个已经代理过了的对象。
  4. 如果是代理过的对象原路返回,否则判断目标对象是否有相应代理,有直接取出响应代理对象,否则继续向下。
  5. 针对不同的值类型处理。
  6. 代理整个 trarget,把当前代理的对象设置到 weakmap 中,将代理完的对象返回。

3、ref 和reactive 的区别

1、可接受的原始数据类型不同

ref() 和 reactive() 都是接收一个普通的原始数据,再将其转换为响应式对象。区别在于:ref可以同时处理基本数据类型和对象,而reactive只能处理处理对象而支持基本数据类型。

js 复制代码
const numberRef = ref(0);           // OK
const objectRef = ref({ count: 0 }) // OK

//TS2345: Argument of type 'number' is not assignable to parameter of type 'object'
const numberReactive = reactive(0);
const objectReactive = reactive({ count: 0}); // OK

这是因为二者响应式数据实现的方式不同:

  • ref是通过一个中间对象RefImpl持有数据,并通过重写它的 set 和 get 方法实现数据劫持 的,本质上依旧是通过 Object.defineProperty 对RefImpl的value属性进行劫持。

  • reactive则是通过 Proxy 进行劫持的。Proxy 无法对基本数据类型进行操作,进而导致reactive在面对基本数据类型时的束手无策。

总结:ref可以存储基本数据类型而reactive则不能。

2、返回值类型不同

运行如下代码:

js 复制代码
const count1 = ref(0);
console.log(count1); // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0}

const count2 = reactive({count:0}); 
console.log(count2); // Proxy(Object) {count: 0}

ref()返回的是一个持有原始数据的 RefImpl 实例。而reactive()返回的类型则是原始数据的代理 Proxy 实例

因此,在定义数据类型时,有些许差别:

ts 复制代码
interface Count {
  num: number;
}

const countRef: Ref<number> = ref(0);
const countReactive: Count = reactive({ num: 1 });

另外如果 reactive 中有响应式对象,它会被自动展开,所以下面代码是正确的:

js 复制代码
const countReactiveRef: Count = reactive({ num: ref(2) })

由于 ref 返回的是一个 RefImpl 实例,而reactive 返回的是一个代理,其类型本身还是传入的目标对象的类型。因此,前者可以自己持有依赖(ref 通过 dep 持有依赖),后者则借助全局对象 targetMap 管理依赖(reactive 借助了一个全局的弱引用 Map 存储依赖)。

结论:ref(value: T)返回的 Ref 类型,而 reactive(object: T) 返回的 T 类型的代理,这也导致前者可以靠自身属性管理依赖,而后者则借助全局变量 targetMap 管理依赖。

3、访问数据的方式不同

返回值的类型不同,就会导致数据的访问方式不同。通过上文的可知:

  • ref() 返回的是 RefImpl 的一个实例对象,该对象通过 _value 私有变量持有原始数据,并重写了 value 的 get 方法。因此,当想要访问原始对象的时候,需要通过 xxx.value 的方式触发 get 函数获取数据。同样的,在修改数据时,也要通过 xxx.value = yyy 的方式触发 set 函数。
  • reactive() 返回的是原始对象的代理,代理对象具有和原始对象相同的属性,因此我们可以直接通过 .xxx 的方式访问数据。

例如下面代码:

js 复制代码
const objectRef = ref({ count: 0 });
const refCount = objectRef.value.count;

const objectReactive = reactive({ count: 0}); 
const reactiveCount = objectReactive.count;

总结:ref 需要通过 value 属性间接的访问数据(在template 中 vue 做了自动展开,可以省略 .value),而 reactive 可以直接访问。

4、原始对象的可变性不同

ref 通过一个 RefImpl 实例持有原始数据,进而使用 .value 属性访问和更新。而对于一个实例而言,其属性值是可以修改的。因此可以通过 .value 的方式为 ref 重新分配数据,无需担心 RefImpl 实例被改变进而破坏响应式。

html 复制代码
<template>
  <div>{{ data.count || data.name }}</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const data = ref({ count: 1 });
console.log(data.value.count); // 1

// 修改原始值
data.value = { count: 3 };
console.log(data.value.count); // 3

// 修改原始值(数据不会失去响应性)
data.value = { name: 'Karl' };
console.log(data.value.name); // Karl

console.log(data.value.count); // undefined
</script>

而 reactive 返回的是原始对象的代理,因此不能对其重新分配对象,只能通过属性访问修改属性值,否则会破坏掉响应式。

js 复制代码
<template>
  <div>
    <div>{{ objectReactive.count }}</div>
    <button @click="add">add</button>
    <button @click="update">update</button>
    <button @click="getValue">getValue</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, watch } from 'vue';

let objectReactive = reactive({ count: 0 });

// 可以正常修改值,并且页面会响应式更新
function add() {
  objectReactive.count += 1;
}

// 修改objectReactive之后,不再会接收到数据变化的通知
function update() {
  objectReactive = { count: 3 };
  objectReactive.count = 4;
}

// 获取最新的count值
function getValue() {
  console.log(`count值:${objectReactive.count}`);
}

// 监听count的变化
watch(
  () => objectReactive.count,
  newValue => {
    console.log(`数据变化了:${newValue}`);
  }
);
</script>

// 第一步
// 点击两次次add按钮,控制台会依次输出:
// 数据变化了:1
// 数据变化了:2
// 并且页面上的count值也更新了

// 第二步
// 再点击一次update按钮后,控制台没输出,页面上的count值也没更新

// 第三步
// 最后点击一次getValue按钮,控制台输出:
// count值:4
// 但页面上展示的count值还是 2 而不是 4

原因很简单:watch 函数监听的是原始值 { count: 0 } 的代理 objectReactive,此时当通过该代理修改数据时,可以触发回调。但是当程序运行到 objectReactive = { count: 3 } 之后,objectReactive 的指向不再是 { count: 0 } 的代理了,而是指向了新的对象{ count: 3 }。这时 objectReactive.count = 4 修改的不再是 watch 所监听的代理对象,而是新的普通的不具备响应式能力的对象{ count: 3 }。watch 就无法监听到数据的变化了, objectReactive 响应式能力也因此而被破坏了。

如果直接修改 ref 的指向,ref 的响应式也会失效:

js 复制代码
<template>
  <div>
    <div>{{ count }}</div>
    <button @click="add">add</button>
    <button @click="update">update</button>
    <button @click="getValue">getValue</button>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';

let count = ref(0);

// 可以正常修改值,并且页面会响应式更新
function add() {
  count.value = 1;
}

// 直接修改ref的指向之后,不再会接收到数据变化的通知
function update() {
  count = ref(2);
}

// 获取最新的count值
function getValue() {
  console.log(`count值:${count.value}`);
}

// 监听count的变化
watch(
  () => count.value,
  newValue => {
    console.log(`数据变化了:${newValue}`);
  }
);
</script>

// 第一步
// 点击一次add按钮,控制台会输出:
// 数据变化了:1
// 并且页面上的count值也更新了

// 第二步
// 再点击一次update按钮后,控制台没输出,页面上的count值也没更新

// 第三步
// 最后点击一次getValue按钮,控制台输出:
// count值:2
// 但页面上展示的count值还是 1 而不是 2

结论:可以给 ref 的值重新分配给一个新对象,而 reactive 只能修改当前代理的属性。

5、ref借助reactive实现对Object类型数据的深度监听

ref 在发现被监听的原始对象是 Object 类形时,会将原始对象转换成 reactive 并赋值给 _value 属性。而此时 ref.value 返回的并不是原始对象,而是它的代理。

可以通过如下代码验证:

js 复制代码
const refCount = ref({ count: 0 });
console.log(refCount.value); // Proxy(Object) {count: 0}

结论:ref() 在原始数据为 Object 类形时,会通过 reactive 包装原始数据后再赋值给 _value。

6、对侦听属性的影响不同

执行如下代码:

js 复制代码
<template>
  <div>{{ refData.count }}</div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';

const refData = ref({ count: 0 });

watch(refData, () => {
  console.log('refData数据变化了');
});

refData.value = { count: 1 };
</script>

// 输出结果: 
// refData数据变化了

watch() 可以检测到 ref.value 的变化。继续执行如下代码:

js 复制代码
<template>
  <div>
    <div>{{ refData.count }}</div>
    <div>{{ reactiveData.count }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, watch } from 'vue';

const refData = ref({ count: 0 });

watch(refData, () => {
  console.log('refData数据变化了');
});

refData.value.count = 1;

const reactiveData = reactive({ count: 0 });

watch(reactiveData, () => {
  console.log('reactiveData数据变化了');
});

reactiveData.count = 1;

// 输出结果: 
// reactiveData数据变化了
</script>

这次 watch() 没有监听到 refData 的数据变化。watch() 默认情况下不会深入观察 ref。若要 watch 深入观察 ref,则需要修改参数如下:

js 复制代码
watch(
  refData,
  () => {
    console.log('refData数据变化了');
  },
  { deep: true }
);

而对于 reactive 而言,无论你是否声明 deep: true,watch 都会深入观察。

结论:watch() 默认情况下只监听 ref.value 的更改,而对 reactive 执行深度监听。

7、总结和用法

  • ref 可以存储原始类型,而 reactive 不能。

  • ref 需要通过 <ref>.value 访问数据,而 reactive() 可以直接用作常规对象。

  • 可以重新分配一个全新的对象给 ref 的 value 属性,而 reactive() 不能。

  • ref 类型为 Ref<T>,而 reactive 返回的反应类型为原始类型本身。

  • 基于第四条,ref 可以自身管理依赖,而 reactive 则借助全局变量以键值对的形式进行管理(weakmap)。

  • watch 默认只观察 ref 的 value,而对 reactive 则执行深度监听。

  • ref 默认会用 reactive 对 Object 类型的原始值进行深层响应转换。

8、比较

reactive ref
👎 只对对象类型起作用 👍对任何类型起作用
👍在<script>和<template>中访问值没有区别 👎访问<script>和<template>中的值的行为不同
👎重新赋值一个新的对象会"断开"响应式 👍对象引用可以被重新赋值
属性可以直接访问 需要使用.value来访问属性
👍引用可以通过函数进行传递
👎解构的值不是响应式的
👍与Vue2的data对象相似

9、使用场景

  • 基础类型值(String,Number,Boolean,Symbol) 或单值对象(类似 { count: 1 } 这样只有一个属性值的对象) 使用 ref。
  • 引用类型值(Object、Array)使用 reactive。

4、toRef

toRef 用于创建一个指向响应式对象属性的引用。它的作用是将一个响应式对象的属性变成一个单独的引用,这个引用可以像普通变量一样传递和使用,但仍然保持响应式。

4.1、toRef是什么

  • toRef 可以响应对象 Object,其针对的是某一个响应式对象(reactive 封装)的属性 prop。

  • toRef 和对象 Object 两者保持引用关系,即一个改完另外一个也跟着改。

  • toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式。

  • toRef 作用是创建一个 ref 对象,其 value 值指向另一个对象中的某个属性值,与原对象是存在关联关系的。

  • toRef() 只能处理一个属性,但是 toRefs() 却可以一次性批量处理。

toRef 可以用来为源响应式对象 上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。

4.2、举个例子🌰

对于一个普通对象来说,如果这个普通对象要实现响应式,就用 reactive。用了 reactive 之后,它就在响应式对象里面。那么在一个响应式对象里面,如果其中有一个属性 要拿出来单独做响应式的话,就用 toRef 。来举个例子看一看:

js 复制代码
<template>
  <div>
    <span>姓名:{{ data.name }}</span>
    <span>年龄:{{ ageRef }}</span>
  </div>
</template>

<script setup lang="ts">
import { reactive, toRef } from 'vue';

// 响应式对象
const data = reactive({ name: '张三', age: 18 });

// 普通对象(toRef 如果用于非响应式对象,产出的结果不具备响应式)
// const state = {
//   name: 'monday',
//   age: 12,
// };

// 实现某一个属性的数据响应式
const ageRef = toRef(data, 'age');

setTimeout(() => {
  data.age = 20;
}, 1500);

setTimeout(() => {
  ageRef.value = 25;
}, 3000);
</script>

我们通过 reactive 来创建一个响应式对象 ,之后如果只单独要对响应式对象里面的某一个属性 进行响应式,那么使用 toRef 来解决。用 toRef(Object, prop) 的形式来传对象名 和具体的属性名,达到某个属性数据响应式的效果。

4.3、ref和toRef的区别

ref toRef
ref接收一个参数:ref(原始值) toRef接收两个参数:toRef(Proxy, 'xxprop')
本质是拷贝,修改响应式数据不会影响原始数据 本质是引用关系,修改响应式数据会影响原始数据

5、toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref。

5.1、toRefs是什么

  • 与 toRef 不一样的是, toRefs 是针对整个对象的所有属性,目标在于将响应式对象(reactive 封装)转换为普通对象。

  • 普通对象里的每一个属性 prop 都对应一个 ref。

  • toRefs 和对象 Object 两者保持引用关系,即一个改完另外一个也跟着改。

  • 通过 toRefs 转换后的引用,访问属性需要使用 .value,而不是直接访问属性名。在模板中使用时,不需要额外的 .value,因为 Vue 3 会自动处理。

  • toRefs 可以用来解构 Proxy(Proxy 如果解构,基本类型数据会丢失响应式)。

  • toRefs 在调用时只会为源对象上可以枚举的属性 创建 ref。如果要为可能还不存在的属性创建 ref,则改用 toRef。

  • toRefs 不创造响应式 (那是reactive的事情),它本身只是延续响应式 ,让一个非响应式数据通过 toRef 或 toRefs 转换为一个具备响应式的数据。

合成函数 返回响应式对象 时,使用 toRefs

可以看到 toRefs 和 toRef 很像,从名字上他们长的差不多,从作用看他们确实也是差不多。只不过一个是对【对象】属性单个处理,一个是针对整个对象进行批量处理。

5.2、举个例子🌰

假设我们要将一个响应式对象里面的元素解构出来,那么我们可以这么处理。代码如下:

js 复制代码
<template>
  <div>
    <span>姓名:{{ name }}</span>
    <span>年龄:{{ age }}</span>
  </div>
</template>

<script setup lang="ts">
import { reactive, toRefs } from 'vue';

const data = reactive({ name: '张三', age: 18 });

// 将响应式对象,变成普通对象
const { name, age } = toRefs(data);

// 等价于
// const name = toRef(data, 'name');
// const age = toRef(data, 'age');

// 直接解构会失去响应性
// const { name, age } = data;

setTimeout(() => {
  console.log('name1:', data.name);
  console.log('age1:', data.age);
  console.log('refName1:', name);
  console.log('refAge1:', age);

  // 修改值(经过了toRef的处理,修改变量的值就需要xxx.value)
  name.value = '李华';
  age.value = 40;

  // 效果一样
  // data.name = '李华';
  // data.age = 40;
  
  console.log('name2:', data.name);
  console.log('age2:', data.age);
  console.log('refName2:', name.value);
  console.log('refAge2:', age.value);
}, 1500);
</script>

// 输出结果:
// name1: 张三
// age1: 18
// refName1: ObjectRefImpl {_object: Proxy(Object), _key: 'name', _defaultValue: undefined, __v_isRef: true}
// refAge1: ObjectRefImpl {_object: Proxy(Object), _key: 'age', _defaultValue: undefined, __v_isRef: true}

// name2: 李华
// age2: 40
// refName2: 李华
// refAge2: 40

5.3、为什么需要toRef和toRefs

与 ref 不一样的是, toRef 和 toRefs 这两个兄弟,它们不创造响应式 ,而是延续响应式 。创造响应式一般由 ref 或者 reactive 来解决,而 toRef 和 toRefs 则是把对象的数据进行分解和扩散,其这个对象针对的是响应式对象 而非普通对象 。总结起来有以下三点:

  • 初衷: 在不丢失响应式 的情况下,把对象数据进行分解或扩散。
  • 前提: 针对的是响应式对象 ( reactrive 封装的)而非普通对象。
  • 注意: 不创造 响应式,而是延续响应式。

6、isRef

使用 ref 函数可以创建一个响应式的引用对象。而 isRef 函数可以用来判断一个值是否是这种引用对象,返回布尔值。

6.1、类型

ts 复制代码
function isRef<T>(r: Ref<T> | unknown): r is Ref<T>

请注意,返回值是一个类型判定 (type predicate),这意味着 isRef 可以被用作类型守卫:

ts 复制代码
let foo: unknown
if (isRef(foo)) {
  // foo 的类型被收窄为了 Ref<unknown>
  foo.value = 10;
}

6.2、作用

判断某个值是否为 Ref 对象,如果是 Ref 对象的话,它就是响应式的,且需要通过 .value 取值或赋值。

另外可以做类型保护:

上面的 is 是一个 ts 中的类型谓词,用来帮助 ts 编译器 收窄 变量的类型。例如:

ts 复制代码
function isString(test: any): test is string{
  return typeof test === "string";
}

isString 函数返回一个类型判定,意思是当 test 的类型是 string 时,那么该函数的返回值类型就是 string,你可以放心地把它当做 string 类型使用。

将isRef(foo)用作类型保护时,它能确保你的 .value 操作不会出现任何问题。

6.3、举个例子🌰

js 复制代码
<script setup lang="ts">
import { ref, reactive, toRef, toRefs, isRef } from 'vue';

const countRef = ref(0);
const dataReactive = reactive({ data: 30, info: { name: 'jk', age: 12 } });
const data1 = toRef(dataReactive, 'data');
const { data: data2, info } = toRefs(dataReactive);

console.log(isRef(countRef)); // 输出:true
console.log(isRef(123)); // 输出:false
console.log(isRef(dataReactive)); // 输出:false
console.log(isRef(data1)); // 输出:true
console.log(isRef(data2)); // 输出:true
console.log(isRef(info)); // 输出:true
</script>

7、unref

用于解除一个可能是响应式对象或 ref 的引用。如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

7.1、类型

ts 复制代码
function unref<T>(ref: T | Ref<T>): T

示例:

ts 复制代码
function useFoo(x: number | Ref<number>) { 
  const unwrapped = unref(x); // unwrapped 现在保证为 number 类型 
}

7.2、作用

让你更方便快捷地获取 Ref 对象的 value 值,不用自己重复写判断代码。因为 Ref 对象需要 .value 取值,所以才有了这个函数。

Ref 在模板中的自动解包源码实现就是用的这个方法。

7.3、举个例子🌰

js 复制代码
<script setup lang="ts">
import { ref, reactive, toRef, toRefs, unref } from 'vue';

const countRef = ref(0);
const dataReactive = reactive({ data: 30, info: { name: 'jk', age: 12 } });
const data1 = toRef(dataReactive, 'data');
const { data: data2, info } = toRefs(dataReactive);

console.log(unref(countRef)); // 输出:0
console.log(unref(123)); // 输出:123
console.log(unref(dataReactive)); // 输出:Proxy(Object) {data: 30, info: {...}}
console.log(unref(data1)); // 输出:30
console.log(unref(data2)); // 输出:30
console.log(unref(info)); // 输出:Proxy(Object) {name: 'jk', age: 12}
</script>

8、customRef

用于创建自定义的响应式引用的函数。它可以用来创建一个具有自定义 getter 和 setter 的响应式属性,从而可以在 getter 和 setter 中添加额外的逻辑。customRef 主要用于处理一些特殊的响应式逻辑,例如对属性访问的代理,以及对属性值的访问控制等。

8.1、介绍

customRef 接受一个工厂函数作为参数,该工厂函数返回一个对象,包含 get 和 set 方法。在这些方法中,你可以定义自己的响应式逻辑。通过 customRef 这个方法可以自定义一个响应式的 ref 方法。

8.2、举个例子🌰

js 复制代码
<template>
  <div>
    <p>Value: {{ value }}</p>
    <button @click="increment">Increment</button>
    <p>Access Count: {{ accessCount }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, customRef } from 'vue';
import type { Ref } from 'vue';

// 自定义响应式引用
function trackAccess(_ref: Ref<number>) {
  /**
   * customRef函数返回一个对象,对象里面有2个方法,get/set方法,创建的对象获取数据的时候能访问到get方法,创建的对象修改值的时候会触发set方法
   * customRef函数有2个参数,track/trigger
   * @param track track参数是追踪的意思,get的方法里面调用,可以随时追踪数据改变
   * @param trigger trigger参数 是触发响应的意思,set方法里面调用可以更新UI界面
   */
  return customRef((track, trigger) => ({
    get() {
      track(); // 跟踪访问
      return _ref.value;
    },
    set(newValue) {
      _ref.value = newValue;
      trigger(); // 触发更新
    }
  }));
}

const value = ref(0);
const accessCount = trackAccess(value);

function increment() {
  value.value += 1;
}
</script>

9、shallowRef

用于创建浅响应式引用的函数。与普通的 ref 类似。shallowRef 也可以用来创建一个响应式的引用,但是它有一个特殊的特点:shallowRef 定义的是基本类型数据,数据是响应式数据;定义是的复杂类型数据(对象、数组等),数据不是响应式数据。

举个例子🌰:

js 复制代码
<template>
  <div>
    <p>
      ref基本类型数据A: {{ refDataA }}
      <button @click="changeRefDataA">改变ref基本类型数据A</button>
    </p>
    <p>
      ref复杂类型数据B: {{ refDataB.innerB }}
      <button @click="changeRefDataB">改变ref复杂类型数据B</button>
    </p>
  </div>
</template>

<script setup lang="ts">
import { shallowRef } from 'vue';

// 定义基本类型数据
const refDataA = shallowRef(0);

const changeRefDataA = () => {
  refDataA.value += 1;
};

// 定义复杂类型数据
const refDataB = shallowRef({ innerB: 0 });

const changeRefDataB = () => {
  refDataB.value.innerB += 1;
};
</script>

点击按钮发现:A可以改变, B不行。

10、shallowReactive

用于创建一个浅响应式的对象。与 reactive() 不同,shallowReactive() 只会对对象的第一层属性进行响应式处理,不会递归地将嵌套对象的属性转换为响应式。

这意味着,通过 shallowReactive() 创建的对象,只有第一层属性会自动触发响应式更新,而嵌套的属性不会。这在某些场景下可以优化性能,避免不必要的响应式更新。

举个例子🌰:

js 复制代码
<template>
  <div>
    <button @click="changeFirstData">改变第一层的数据</button>
    <div>姓名:{{ obj.name }}</div>
    <div>年龄:{{ obj.age }}</div>
    <div>城市:{{ obj.address.city }}</div>
    <div>国家码:{{ obj.address.country }}</div>
    <button @click="changeDeepData">改变深层的数据</button>
  </div>
</template>

<script setup lang="ts">
import { shallowReactive } from 'vue';

// 定义复杂类型数据
const obj = shallowReactive({
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA'
  }
});

// 只有第一层数据是响应式数据
const changeFirstData = () => {
  obj.name = 'Tom';
  obj.age = 22;
};

// 深层数据不是响应式数据
const changeDeepData = () => {
  obj.address.country = 'CN';
};
</script>

点击按钮发现:shallowReactive 只处理对象内最外层属性,第一层数据会改变,深层数据不会改变。

11、toRaw

用于获取由响应式对象包装的原始非响应式对象。它可以用来访问对象的原始值,而不会触发响应式系统的追踪和更新。简单点说就是用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。

根据一个 Vue 创建的代理返回其原始对象。 toRaw() 可以返回由 reactive()、readonly()、shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

举个例子🌰:

js 复制代码
<template>
  <div>
    <div>姓名:{{ person.name }}</div>
    <div>年龄:{{ person.age }}</div>
    <div>朋友:{{ person.friend.name }}-{{ person.friend.age }}</div>
    <div>
      爱好列表:
      <div v-for="(item, index) in person.hobbies" :key="index">
        <div>{{ item }}</div>
      </div>
    </div>
    <button @click="updateInfo">修改信息</button>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, toRaw } from 'vue';
const person = {
  name: '艾薇儿',
  age: 18,
  friend: {
    name: '安妮·海瑟薇',
    age: '28'
  },
  hobbies: ['music', 'dance', 'movie']
}

const reactivePerson = reactive(person);
console.log('reactivePerson:', reactivePerson); // reactivePerson: Proxy(Object){name: '艾薇儿', age: 18, friend: {...}, hobbies: Array(3)}
console.log('rowPerson:', toRaw(reactivePerson)); // rowPerson: {name: '艾薇儿', age: 18, friend: {...}, hobbies: Array(3)}
console.log(toRaw(reactivePerson) === person); // true

const updateInfo = () => {
  person.age = 30;
  person.friend.name = '疯驴子';
  console.log('person:', person); 
  // person: 
  // {
  //   name: '艾薇儿',
  //   age: 30,
  //   friend: {
  //     name: '疯驴子',
  //     age: '28'
  //   },
  //   hobbies: ['music', 'dance', 'movie']
  // }
  console.log('reactivePerson:', reactivePerson); 
  // reactivePerson: 
  // {
  //   name: '艾薇儿',
  //   age: 18,
  //   friend: {
  //     name: '安妮·海瑟薇',
  //     age: '28'
  //   },
  //   hobbies: ['music', 'dance', 'movie']
  // }
};
</script>

此时toRaw(reactivePerson)就是个普通对象,更新时不会引起页面渲染。

toRaw的使用场景

用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。

12、markRaw

用于标记一个对象,使它在响应式系统中变为"非响应式"的。这意味着标记为 markRaw 的对象将不会被转换为响应式代理,也不会触发响应式更新。

markRaw将一个对象标记为不可被转为代理。返回该对象本身。

作用:标记一个对象,使其永远不会再成为响应式对象;

如果不想要数据被追踪,变成响应式数据可以调用这个方法,就无法 追踪修改数据重新渲染页面。

举个例子🌰:

js 复制代码
<template>
  <button @click="changeName">修改名字</button>
  <div>姓名:{{ student.name }}</div>
  <button @click="changeInfo">修改信息</button>
  <div>年龄:{{ student.info.age }}</div>
  <div>性别:{{ student.info.sex }}</div>
  <button @click="getData">获取最新数据</button>
</template>

<script setup lang="ts">
import { reactive, markRaw } from 'vue';
const info = { age: 11, sex: 'man' };

const student = reactive({
  name: 'kun',
  info: markRaw(info)
});

const changeName = () => {
  // 会触发响应式更新
  student.name = 'iKun';
};

const changeInfo = () => {
  // 不会触发响应式更新
  student.info.age = 26;
  student.info.sex = 'woman';
};

const getData = () => {
  console.log('info:', info);
  // info:
  // {
  //   age: 26,
  //   sex: 'woman'
  // }
  console.log('student:', student);
  // student:
  // {
  //   name: 'iKun',
  //   info: {
  //     age: 26,
  //     sex: 'woman'
  //   }
  // }
};
</script>

修改名字时数据更新并导致页面会重新渲染,修改信息时数据还是会更新但页面不会渲染。

markRaw的使用场景

  • 有些值不应被设置为响应式的,如:复杂的第三方类库等;
  • 当渲染具有不可变数据源的大列表时,跳过响应式转换可提高性能;

13、readonly

用于声明对象属性,使其成为只读属性,即该属性只能被读取,而不能被重新赋值。简单来说就是将响应式数据变成只读的数据(深只读)

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。要避免深层级的转换行为,请使用 shallowReadonly() 作替代。

举个例子🌰:

js 复制代码
<template>
  <div>
    <div>
      {{ count }}
      <button @click="changeCount">changeCount</button>
    </div>
    <div>
      {{ obj.name }} {{ obj.info.age }}
      <button @click="objAge">objAge</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive, readonly, ref } from 'vue';
const count = readonly(ref(0));
const obj = readonly(
  reactive({
    name: '张三',
    info: {
      age: 18
    }
  })
);

const changeCount = () => {
  // 修改只读对象会触发警告,但不会报错也不会生效
  count.value += 1; // [Vue warn] Set operation on key "value" failed: target is readonly.
  console.log(count.value); // 0
};
const objAge = () => {
  obj.name = '李四'; // [Vue warn] Set operation on key "name" failed: target is readonly.
  obj.info.age += 1; // [Vue warn] Set operation on key "age" failed: target is readonly.
  console.log('obj:', obj);
  // obj:
  // {
  //   name: '张三',
  //   info: {
  //     age: 18
  //   }
  // }
};
</script>

14、shallowReadonly

用于创建一个浅层只读(read-only)的代理对象。这个代理对象将会使对象的属性变为只读,但不会递归地将对象的嵌套属性转换为只读。

这意味着,使用 shallowReadonly 创建的代理对象只会影响对象的一层属性,而不会影响嵌套在其中的对象的属性。

使用 shallowReadonly 主要用于那些你希望只读限制不深入到嵌套对象的情况,这在某些场景下可以提供更灵活的控制。例如,你可能希望保持某个对象的结构不变,但限制对它的直接属性的修改。

readonly() 的浅层作用形式,和 readonly() 不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

举个例子🌰:

js 复制代码
<template>
  <div>
    <button @click="change">按钮</button>
    <div>浅层数据:{{ obj.age }}</div>
    <div>深层数据:{{ obj.data.data1.data2.abc }}</div>
  </div>
</template>

<script setup lang="ts">
import { reactive, shallowReadonly } from 'vue';

let obj = reactive({
  name: '张三',
  age: 18,
  data: {
    data1: {
      data2: {
        abc: 0
      }
    }
  }
});

obj = shallowReadonly(obj);
// 点击改变数据
const change = () => {
  // 修改浅层只读对象的直接属性会触发警告,但不会报错并且也不会生效
  obj.age += 1; // [Vue warn] Set operation on key "age" failed: target is readonly.

  // 添加新属性会触发警告并且也不会生效
  obj.newProperty = 'test'; // [Vue warn] Set operation on key "newProperty" failed: target is readonly.

  // 修改浅层只读对象的嵌套属性不会触发警告并且会生效
  obj.data.data1.data2.abc = 2;

  console.log(obj.age); // 18
  console.log(obj.newProperty); // undefined
  console.log(obj.data.data1.data2.abc); // 2
};
</script>

使用场景:

其他组件传递过来的信息,第一层数据只希望展示或者使用,不希望修改(别人给你数据,只有第一层不让改,其他层数据不受影响)。

相关推荐
qwfy5 小时前
我要用 AI-native 架构,从零重建一套制造业 ERP/MES
vue.js·spring boot·架构
dsyyyyy11015 小时前
Vue 3 Watch 监视完全指南
前端·javascript·vue.js
瞬时出道6 小时前
AI 回答为什么能一个字一个字蹦出来?前端搞懂 SSE 这一篇就够了
vue.js
u0111026758 小时前
Vue 3 实现图片裁剪框:拖动、缩放与固定宽高比
前端·javascript·vue.js
小小善后师8 小时前
用 Canvas + AI 实现登录页的 Logo 粒子动画
前端·vue.js
web打印社区10 小时前
远程打印:WebSocket 与 HTTP 轮询怎么选
前端·vue.js·websocket·网络协议·http·electron·pdf
朱 欢 庆12 小时前
el-table-v2 虚拟表格组件
前端·javascript·vue.js
今年下半年1 天前
【VUE】整合腾讯地图、自定义区域边界、村委名称及资产统计(放大显示资产点位)
前端·javascript·vue.js
今年下半年1 天前
【前端】ant-design-vue 表格「行、列都动态」的处理方案
前端·javascript·vue.js
志尊宝1 天前
Vue3 零基础每日笔记(054):Pinia 三件套详解——state / getters / actions 全搞懂
前端·javascript·vue.js·vue·前端开发