了解 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 做了什么
- 调用
reactive
方法传数据,判断对象是否是只读对象,如果是直接返回"无法操作",否则向下继续执行。 - 调用
createReactiveObject
返回一个代理对象。 - 判断传入的值是不是一个对象,如果不是则直接原路返回,否则判断
target
是不是一个已经代理过了的对象。 - 如果是代理过的对象原路返回,否则判断目标对象是否有相应代理,有直接取出响应代理对象,否则继续向下。
- 针对不同的值类型处理。
- 代理整个
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>
使用场景:
其他组件传递过来的信息,第一层数据只希望展示或者使用,不希望修改(别人给你数据,只有第一层不让改,其他层数据不受影响)。