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 类。

  • 创建类的私有变量 ,保存传入的valueshallow

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

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

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

  • 触发更新。

1.4、为什么需要用ref

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

1.5、为何ref需要.value属性

  • ref 是一个对象 ,这个对象不丢失响应式,且这个对象用 value 来存储值。
  • 因此,通过 .value 属性的 getset 来实现响应式。
  • 只有当用于 模板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 等函数创建的响应式数据时不能直接使用 ++-- 等一元运算符。这是因为一元运算符在背后会调用目标对象的 getset 操作,而 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持有数据,并通过重写它的 setget 方法实现数据劫持 的,本质上依旧是通过 Object.definePropertyRefImplvalue属性进行劫持。

  • 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 私有变量持有原始数据,并重写了 valueget 方法。因此,当想要访问原始对象的时候,需要通过 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 属性间接的访问数据(在templatevue 做了自动展开,可以省略 .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: truewatch 都会深入观察。

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

7、总结和用法

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

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

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

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

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

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

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

8、比较

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

9、使用场景

  • 基础类型值(StringNumberBooleanSymbol) 或单值对象(类似 { count: 1 } 这样只有一个属性值的对象) 使用 ref
  • 引用类型值(ObjectArray)使用 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 都是指向原始对象相应 propertyref

5.1、toRefs是什么

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

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

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

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

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

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

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

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

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

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

  • 初衷:不丢失响应式 的情况下,把对象数据进行分解或扩散
  • 前提: 针对的是响应式对象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

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

8.1、介绍

customRef 接受一个工厂函数作为参数,该工厂函数返回一个对象,包含 getset 方法。在这些方法中,你可以定义自己的响应式逻辑。通过 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>

使用场景:

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

相关推荐
四喜花露水11 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
程序员爱技术5 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
cs_dn_Jie9 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic10 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿10 小时前
webWorker基本用法
前端·javascript·vue.js
customer0811 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
getaxiosluo12 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v12 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
栈老师不回家13 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙13 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js