# Vue 组合式 API - setup、reactive 与 ref,响应式工具
TIP
组合式 API(Composition API)是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:
- 响应式 API (opens new window):使我们可以直接创建响应式状态、计算属性和侦听器,如:
ref()
和reactive()
- 生命周期钩子 (opens new window):使我们可以在组件各个生命周期阶段添加逻辑,如:
onMounted()
和onUnmounted()
- 依赖注入 (opens new window):使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统,如
provide()
和inject()
组合式 API 是 Vue 3 及 Vue 2.7 (opens new window) 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api
(opens new window)。
在 Vue 3 中,组合式 API 基本上都会配合
<script setup>
(opens new window) 语法在单文件组件中使用。
下面是一个使用组合式 API 的组件示例:
<script setup>
import { ref, reactive, computed } from "vue";
// 创建响应式数据 相当于选项式API中 data中定义的属性
const msg = ref("Hello Composition API");
const num = ref(100);
// 定义一个方法,相当于选项式API中的methods选项中定义的方法
function update() {
num.value = 200;
}
// 创建一个计算属性,相当于选项式API中的computed选项中创建的计算属性
const price = computed(() => {
return "$" + num.value;
});
</script>
<template>
<h3>{{ msg }}</h3>
<p>价格:{{ price }}</p>
<button @click="update">更新</button>
</template>
注:
- 从上面代码中可以看到,创建的响应式数据,methods 方法,computed 计算属性都是基于函数来实现的。这些函数组合在一起就能完成一个具体的应用。
- 所谓组合式 API 就是指这些不同的 API(函数)组合在一起来实现一个完整的应用,和之前学习的选项式 API 风格完全不同。
以上代码最终渲染结果如下:
TIP
接下来我们将正式开启组合式 API 的学习之旅,本章节具体内容安排如下:
- 初识
setup()
函数 - 初始
reactive
与ref
响应式 API - 深入响应式 API- 工具函数(一)
- 深入响应式 API- 工具函数(二)
setup()
函数参数
# 一、初识 setup() 函数
TIP
setup()
函数是在组件中使用组合式 API 的入口(表演的舞台),所有组合式 API 代码都写在setup()
函数中
<script>
export default {
// setup函数是所有组合式API的入口(表演的舞台)
setup() {
// .....所有代码在此书写
},
};
</script>
# 1、setup() 函数的返回值
TIP
setup()
函数的返回值通常是一个对象,这个对象的所有属性会暴露给组件模板和组件实例,所以
- 在组件的模板中可以直接访问
setup()
返回对象的属性 - 在组件的选项式 API中可通过组件实例可以访问
setup()
返回对象身上的属性。
以下代码展示了,在模板中和组件实例上访问到setup()
函数返回的对象身上的属性和方法。
提示: 选项式 API 与组合式 API 是可以共存的。
<script>
export default {
setup() {
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
message: "Hello Vue!!",
sayHello: () => console.log("sayHello"),
};
},
beforeCreate() {
console.log(this.message); // 组件实例可以直接获取message属性
},
};
</script>
<template>
<button @click="sayHello">sayHello</button>
<!--模板中可以直接使用message属性-->
<div>{{ message }}</div>
</template>
最终渲染效果如下:
# 2、setup() 函数中 this 指向
TIP
setup()
函数自身并不含对组件实例的访问权,在setup()
中访问this
会是undefind
- 所以
setup()
函数内是没有办法访问到选项式 API 中的属性、方法、计算属性等。
<!-- 以下代码是错的 -->
<script>
export default {
data() {
return {
message: "Hello Vue!!",
};
},
setup() {
console.log(this); // undefined
console.log(this.message); // 抛出错误,因为this为undefined
},
};
</script>
# 3、setup() 函数执行时机
TIP
setup()
函数是在 Vue 生命周期函数beforeCreate()
之前被自动调用的。
所以在beforeCreate()
函数和选项式 API 中可以访问到setup()
函数对外暴露的属性
<script>
export default {
data() {
return {
text: this.message,
};
},
setup() {
return {
message: "Hello Vue!!",
};
},
beforeCreate() {
// 在控制台输出 : beforeCreate: Hello Vue!!
console.log("beforeCreate:", this.message);
},
};
</script>
<template>
<!--以下代码渲染后效果: <div> Hello Vue!! </div>-->
<div>{{ text }}</div>
</template>
# 4、setup() 函数暴露非响应式属性
TIP
以下方式setup()
函数对外暴露的属性非响应式的,当属性的值发生变化时,页面并不会同步更新。
<script>
export default {
setup() {
let a = 1;
let b = 2;
function update() {
a = 10;
b = 20;
}
return {
a,
b,
update,
};
},
};
</script>
<template>
<div>a的值:{{ a }}</div>
<div>b的值:{{ b }}</div>
<button @click="update">更新a,b的值</button>
</template>
注:
以上的 a,b 属性都是非响应式的,所以当我们点击按扭时,页面中 a,b 的值并没有变化
如果想要对外暴露的属性支持响应式,需要用到响应式 API 中的
reactive()
或ref()
方法来实现。
# 5、总结
TIP
setup()
函数返回值通常是一个对象,这个对象的所有属性会暴露给组件模板和组件实例setup()
函数中的this
指向undefined
setup()
函数会在所有生命周期函数beforeCreate
之前被执行。setup()
函数内定义的变量默认为非响应式的,所以对外暴露该属性为非响应式
# 二、初始 reactive 与 ref 响应式 API
TIP
本小节我们将会初步认识reactive
与ref
两个响应式 API,掌握他们的基本用法。
# 1、初识 reactive() 方法
TIP
reactive()
方法用来返回一个对象的响应式代理。
const objProxy = reactive(obj); // objProxy为obj对象的响应式代理
我们通过响应式代理来操作对象的属性,当属性的值发生变化时也会驱动页面视图的更新。
<script>
// 导入reactive方法
import { reactive } from "vue";
export default {
setup() {
const obj = { a: 1, b: 2 };
// Info为响应式代理对象
const objProxy = reactive(obj);
console.log(objProxy); // Proxy(Object) {a: 1, b: 2}
function update() {
// 修改对象属性的值
objProxy.a = 10;
objProxy.b = 20;
}
// 将属性暴露给组件实例
return {
objProxy,
update,
};
},
};
</script>
<template>
<div>a的值:{{ objProxy.a }}</div>
<div>b的值:{{ objProxy.b }}</div>
<button @click="update">更新a,b的值</button>
</template>
以上代码最终渲染效果如下,当点击按扭更新 a、b 值时,页面中 a、b 的值会更新为最新的。
代码分析
- 以上代码中的
objProxy
为obj
对象的响应式代理对象。 - 我们可以通过
objProxy.a=10
和objProxy.b=20
来为obj
对象的 a、b 属性赋值。 - 当点击按扭后,会调用 update 方法,更新对象的属性的值。最终 a、b 属性的值从 1,2 修改成 10,20。因为数据是响应式的,所以数据更新后页面视图也发生了更新
以上过程证明
objProxy
代理对象具有响应性,所以objProxy
为响应式代理对象。
# 1.1、深层响应性
TIP
通过reactive()
方法转换的响应式对象是 "深层响应" 的。
即不管对象的属性嵌套有多深,都具有响应性,其值发生变化时,页面视图会同步更新。
<script>
import { reactive } from "vue";
export default {
setup() {
// info为响应式代理对象
const info = reactive({
a: {
b: {
c: [1, 2, 3],
},
},
});
function add() {
info.a.b.c.push("A");
}
return {
info,
add,
};
},
};
</script>
<template>
<ul>
<li v-for="item in info.a.b.c">{{ item }}</li>
</ul>
<button @click="add">添加</button>
</template>
以上代码渲染后效果如下:
当点击按扭后,往数组中添加了“A",页面同步更新渲染出来了。
# 1.2、reactive() 无法转换基本数据类型
TIP
reactive()
方法只能将一个对象转换为一个响应式对象,而不能将一个基本数据类型转换为响应式对象。- 因为
reactive()
方法的底层采用的是Proxy
来实现的,而Proxy
只能创建对象的代理。
以下代码为错误示例
import { reactive } from "vue";
export default {
setup() {
let msg = reactive("Hello Vue!!");
console.log(msg);
},
};
以上代码执行后,在控制台打印如下结果:
注:
警告提示,值不能被设置为响应式,最后 msg 的结果仍为原始值"Hello Vue!!"
# 1.3、reactive() 方法底层实现原理
TIP
reactive()
方法返回的响应式代理对象本质是Proxy()
的实例。
const objProxy = reactive(obj);
以上代码的内部实现如下:(以下为极简版, 主要帮助大家理解响应式代理)
function reactive(obj) {
// 判断obj是不是对象,是对象才可以利有Proxy实现代理
if (obj !== null && typeof obj === "object") {
// objProxy响应式代理对象
return new Proxy(obj, {
// 当访问objProxy对象身上的属性时,get方法会被调用
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(); // 响应式依赖收集 ,收集属性对应的DOM更新代码
return res;
},
// 当给objProxy对象向上的属性赋值时,set方法会被调用
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
trigger(); // 触发响应式依赖 更新DOM
},
// ....
});
}
}
# 2、初识 ref() 方法
TIP
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
- 可更改是指你可以为
ref
对象的value
属性赋予新的值 - 响应式是指所有对
.value
的操作都将被追踪,并且写操作会触发与之相关的副作用(DOM 更新)
<script>
import { ref } from "vue";
export default {
setup() {
// msg为一个ref对象,并且是响应式的
const msg = ref("Hello Vue!!"); // {value: "Hello Vue!!"}
// 修改ref对象value属性的值
msg.value = "Hello ref!!";
return {
msg,
};
},
};
</script>
<template> {{ msg }} </template>
注:
以上代码中的字符串"Hello Vue!!"
被ref()
方法转换成了一个 ref 对象{value: "Hello Vue!!"}
。
当我们修改这 value 属性的值时,页面视图中用到该数据的地方会同步更新。
以上代码最终渲染结果如下:
# 2.1、访问 ref 对象的 value 属性
TIP
- 在
setup()
中访问ref
对象的value
值时,需要.value
的形式来访问 - 在组件模板中访问
ref
对象的value
值时,它会自动浅层解包(会自动调用ref.value
),因此你无须再在模板中为它写.value
。 - 在选项式 API 中,通过
this
访问 ref 时也会自动浅层解包
使用场景 | 是否自动浅层解包 |
---|---|
setup() 函数 | 否 |
组件模板 | 是 |
选项式 API | 是 |
代码示例
<script>
// 导入 ref 方法
import { ref } from "vue";
export default {
setup() {
// 将"Hello Vue!!" 转换为响应式,可更改的ref对象
const msg = ref("Hello Vue!!");
console.log(msg);
console.log("setup--", msg.value);
// 更新msg方法
function update() {
// 修改msg的值
msg.value = "Hello ref!!!";
}
return {
msg,
update,
};
},
mounted() {
// msg是一个ref对象,但会自动解包,所以不需要通过this.msg.value来获取值
console.log("mounted", this.msg);
},
};
</script>
<template>
<button @click="update">更新</button>
<!--msg会自动解包,不需要使用 msg.value-->
<div>{{ msg }}</div>
</template>
以上代码最终渲染效果如下:
# 2.2、ref() 方法转换对象
TIP
ref()
方法的参数如果为一个对象(引用类型),则最终返回的ref
对象的value
属性值为该对象的响应式代理对象。- 该 ref 对象是一具有深层响应式,可更改的
ref
对象。
<script>
import { ref } from "vue";
export default {
setup() {
const obj = { msg: "Hello ref!!" };
/*
ref的参数为一个对象,先用reactive()方法返回obj的响应式代理对象
然后将该响应式代理对象赋值给到ref对象的value属性。
*/
const refProxyObj = ref(obj);
console.log(refProxyObj);
console.log(refProxyObj.value);
},
};
</script>
以上代码,最终在控制台输出如下结果:
分析输出结果
ref()
方法的参数如果为一个对象,其内部相当于经历了以下两步:
- 先用
reactive(obj)
方法返回obj
的响应式代理对象proxyObj
- 然后将该响应式代理对象赋值给到 ref 对象的 value 属性,即
{value:proxyObj}
所以最终得到的 ref 对象是一个深层响应式,可更改的
ref
对象。
代码演示
<script>
import { ref } from "vue";
export default {
setup() {
const userInfo = ref({
userName: "清心",
sex: ref("女"),
a: {
b: ref(0),
},
});
function update() {
userInfo.value.userName = "icoding";
//sex a.b 都会一个ref对象,但都会自动解包
userInfo.value.sex = "男";
userInfo.value.a.b = 100;
// 下面这种写法,也支持响应式
// userInfo.value.a={b:100}
}
return {
userInfo,
update,
};
},
};
</script>
<template>
<button @click="update">更新</button>
<!--使用时,全都会自动解包-->
<div>{{ userInfo.userName }} -- {{ userInfo.sex }}</div>
<div>{{ userInfo.a.b }}</div>
</template>
注:
当数据的结构比较深时,深层响应式是非常消耗性能的,因为需要对对象进行深度递归,将每一个被嵌套的属性都转换为响应式。
若要避免这种深层次的转换,请使用
shallowRef()
来替代。
# 2.3、ref() 方法的底层实现
TIP
以上代码中的ref()
方法,返回的ref
对象,底层实现如下(极简版,不完整,仅供了解思路)
function ref(value) {
const refObject = {
get value() {
track(refObject, "value");
return value;
},
set value(newValue) {
value = newValue;
trigger(refObject, "value");
},
};
return refObject;
}
注:
- 当我们访问 ref 对象的 value 属性时,本质是触发了 value 属性的 get 方法。
- 当我们给 ref 对象赋值时,本质是触发了 value 属性的 set 方法。
# 3、总结
TIP
reactive 方法
- 用来返回一个对象的响应式代理对象,其响应性为深层响应,不管对象的属性嵌套有多深,都具有响应性,其值发生变化时,页面视图会同步更新。
- reactive 方法无法转换基本数据类型
- reactive 方法的底层实现是通过
proxy
代理来实现的
ref 方法
- 主要用来将一个基本数据类型值转换为一个响应式,可更改的 ref 对象。
- 访问 ref 对象的属性值时,如果在 setup()函数中,需要打点
.value
属性才能访问,如果在模板或选项式 API 中,则会自动浅层解包,不需要.value
。 - ref 方法的参数如果为对象类型,则返回的 ref 对象的 value 属性值为该对象的 proxy 响应式代理对象。
- ref 方法的底层实现是通过创建一个具有 get value 与 set value 属性的对象来实现。
# 三、深入响应式 API - 工具函数(一)
TIP
通过前面的学习我们知道reactive()
方法的返回值为一个对象的响应式代理。本小节我们针对响应式代理对象做深入的学习,主要内容如下:
- 响应式对象会自动解包 ref 属性
- 不能被自动解包的集合
- 解构响应式对象
toRef()
方法toRefs()
方法shallowReactive()
方法readonly()
方法
注:我们将响应式代理对象简称为响应式对象
# 1、响应式对象会自动解包 ref 属性
TIP
一个响应式对象的属性及嵌套属性的值如果为ref
属性,在模板或setup()
中使用时,会自动解包,同时保持响应性
<script>
import { reactive, ref } from "vue";
export default {
setup() {
const userName = ref("艾编程");
const age = ref(33);
const userInfo = reactive({
userName, // ref对象
age, // ref对象
sex: "男",
test: {
a: {
b: ref(3), // ref对象
},
},
});
function update() {
// userName会自动解包 所以写成 userInfo.userName.value="清心" 是错误的
userInfo.userName = "清心"; // 与 userName.value="清心" 一样支持响应式
userInfo.age = 44; // 与 age.value = "44" 一样支持响应式
userInfo.sex = "女";
userInfo.test.a.b = 100; // 深层响应式
}
return {
userInfo,
update,
};
},
};
</script>
<template>
<button @click="update">更新数据</button>
<!--userInfo的属性为ref对象时,会自动解包-->
<div>{{ userInfo.userName }} -{{ userInfo.age }}--{{ userInfo.sex }}</div>
<div>{{ userInfo.test.a.b }}</div>
</template>
以上代码渲染后效果如下:
# 2、不能被自动解包的集合
TIP
当访问到某个响应式数组或 Map
这样的原生集合类型中的 ref
元素时,不会执行 ref 的解包。
const hobbies = reactive([ref("音乐"), ref("阅读"), ref("跳舞")]);
// 这里需要 .value
console.log(hobbies[0].value);
const map = reactive(new Map([["count", ref(0)]]));
// 这里需要 .value
console.log(map.get("count").value);
# 3、解构响应式对象
TIP
如果我们直接利用解构赋值来解构响应式对象
- 当属性值为基本数据类型或 ref 对象(解包后为基本数据类型)时,解构后会失去响应性
- 当属性值为引用数据类型或 ref 对象(解包后为引用数据类型)时,解构后仍保持响应性
<script>
import { reactive, ref } from "vue";
export default {
setup() {
// 解构响应式对象
let { a, b, c, d } = reactive({
a: ref("a"),
b: ref(["b"]),
c: ["c"],
d: "d",
});
console.log(a); // 'a' 基本数据类型,失去响应式
console.log(b); // Proxy(Array) {0: 'b'} 响应式对象,具有响应性
console.log(c); // Proxy(Array) {0: 'c'} 响应式对象,具有响
console.log(d); // 'd' 基本数据类型,失去响应式
// 更新数据
function update() {
a = "aa";
b[0] = "bb";
c[0] = "cc";
d = "dd";
}
return {
a,
b,
c,
d,
update,
};
},
};
</script>
<template>
<div>a:{{ a }}--b:{{ b[0] }}--c:{{ c[0] }}--{{ d }}</div>
<button @click="update">更新</button>
</template>
最终渲染效果如下图:
注:
观察控制台输出的结果,可以看到解构后
- b 和 c 的值为 proxy 响应式代理对象,具有响应性
- a 和 d 变量的值为基本数据类型,不具有响应性。
所以,点击更新后按扭后,b,c 的值更新后,页面中对应的值也被更新了。
TIP
如果解构的响应式对象为数组或 Map 等原生集合类型
- 只有集合中成员为基本数据类型时会失去响应性
- 集合成员为
ref
类型与引用类型都不会失去响应性,因为ref
成员不会自动解包
<script>
import { reactive, ref } from "vue";
export default {
setup() {
// 解构响应式对象
let [a, b, c, d] = reactive([ref("a"), ref(["b"]), ["c"], "d"]);
console.log(a); // {value:'a'} ref对象,具有响应性
console.log(b); // {value:Proxy(Array){0:'b'}} ref对象,具有响应性
console.log(c); // Proxy(Array) {0: 'c'} 响应式对象,具有响性
console.log(d); // 'd' 基本数据类型,失去响应式
// 更新数据
function update() {
a.value = "aa";
b.value[0] = "bb";
c[0] = "cc";
d = "dd";
}
return {
a,
b,
c,
d,
update,
};
},
};
</script>
<template>
<div>a:{{ a }}--b:{{ b[0] }}--c:{{ c[0] }}--{{ d }}</div>
<button @click="update">更新</button>
</template>
以上代码最终渲染结果如下:
注:
观察上图控制台打印的结果,可以看到
- 变量 a,b 为 ref 对象,具有响应性
- 变量 c 为 proxy 代理对象,具有响应性
- d 变量为基本数据类型,不具有响应性
所以点击更新按扭后,a,b,c 变量的值更新后,页面也同步更新了。
那我们如何保持解构后的变量都具有响应性呢 ?这就需要用到接下来讲到的
toRef()
和toRefs()
方法
# 4、toRef() 方法
TIP
toRef()
方法可以基于响应式对象上的一个属性,创建一个对应的 ref。
这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
语法:
const variable = toRef(proxyObject, key, defaultValue);
proxyObject
为响应式对象key
为 响应式对象对应的原对象的属性defaultValue
默认值,当转换对象身上不存在的值是会返回 undefined,如果有默认值,则会使用默认值
代码演示
<script>
import { reactive, toRef } from "vue";
export default {
setup() {
const obj = { a: 1, b: 2 };
// obj为响应式代理对象
const objProxy = reactive(obj);
// toRef()方法将对象obj的属性a转换为一个响应式的ref对象 {value:1}
let a = toRef(objProxy, "a");
console.log(a); // {value:1}
console.log(a.value); // 访问a的值
// objProxy.a=10 时,对应a.value的值也变成了10,两者保持同步
objProxy.a = 10;
setTimeout(() => {
// a.value=100时,对应objProxy.a的值也变成100,两者保持同步
a.value = 100;
}, 2000);
return {
a,
};
},
};
</script>
<template>
<div>{{ a }}</div>
</template>
以上代码最终渲染结果如下:
注:
通过渲染后的结果可以看到,objProxy.a = 10
与a.value = 100
最终都会影响到 a 变量的值,也可以说影响到objProxy.a
的值,同时都具有响应性。
# 4.1、toRef() 注意事项
toRef()
如果传入的对象非响应式的,则返回该对象上指定属性的ref
对象。(只做了解)
let a = toRef({ a: 1 }, "a"); // {value:1}
// 访问a的值
console.log(a.value); // 1
- 当
toRef()
转换一个对象身上不存在的属性时,返回的 ref 对象的 value 值为undefind
,可以为其指定默认值
const objProxy = reactive({ a: 1, b: 2 });
// toRef()方法将对象{a:1,b:2}的属性a转换为一个响应式的ref对象
let c = toRef(objProxy, "c");
// 访问a的值
console.log(c.value); // undefined
c = toRef(objProxy, "c", "默认值");
console.log(c.value); // 默认值
- 利用
toRef()
也可以返回一个只读的ref对象
,当我们想创建一个只读的变量时可以使用
import { reactive, toRef } from "vue";
const obj = { a: 1, b: 2 };
// obj为响应式代理对象
const objProxy = reactive(obj);
// 返回值为只读的ref对象
let a = toRef(() => objProxy);
// 访问a的值
console.log(a.value);
// 以下赋值操作将抛出错误
// a.value = "ssss"
# 5、toRefs() 方法
TIP
- 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。
- 每个单独的 ref 都是使用
toRef()
(opens new window) 创建的。
语法
toRefs(object); // object为响应式对象
如果我们想要一次性解构一个响应式对象的所有属性,则可以使用
toRefs()
import { reactive, ref, toRefs } from "vue";
export default {
setup() {
const objProxy = reactive({ a: 1, b: 2 });
const obj = toRefs(objProxy);
console.log(obj); // {a:toRef(objProxy,"a"),b:toRef(objProxy,"b")}
const { a, b } = obj;
// a,b为ref对象,所以访问需要.value
console.log(a.value, b.value); // 1 2
},
};
代码演示
<script>
import { reactive, ref, toRefs } from "vue";
export default {
setup() {
const userInfo = reactive({
userName: ref("艾编程"), // ref对象
age: ref(33), // ref对象
sex: "男",
test: {
a: {
b: 3,
},
},
});
// toRefs()转换响应式对象,然后利用解构赋值解构属性
let { userName, age, sex, test } = toRefs(userInfo);
// 因为解构后的每个属性为ref对象,所以需要通过.value形式访问
console.log("userName", userName.value); // userName 艾编程
console.log("age", age.value); // age 33
console.log("sex", sex.value); // sex 男
console.log("test", test.value); // proxy对象
function update() {
userName.value = "清心"; // 失去响应性
age.value = "44"; // 失去响应性
sex.value = "女"; // 失去响应性
test.value.a.b = 100; // 保持响应性
}
return {
userName,
age,
sex,
test,
update,
};
},
};
</script>
<template>
<button @click="update">更新数据</button>
<!--userInfo的属性为ref对象时,会自动解包-->
<div>{{ userName }} -{{ age }}--{{ sex }}</div>
<div>{{ test.a.b }}</div>
</template>
以上代码最终渲染效果如下:
注意事项
toRefs
在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef
(opens new window)。
# 6、shallowReactive() 方法
TIP
shallowReactive()
相当于是reactive()
的浅层作用形式。- 也就是
shallowReactive()
转换的响应式对象只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为ref
的属性不会被自动解包了。
<script>
import { reactive, ref, shallowReactive } from "vue";
export default {
setup() {
const info = shallowReactive({ count: ref(0), a: { b: 1 } });
function update1() {
// ref不会自动解包,需要调用value,值修改后会触发
info.count.value = 100;
}
function update2() {
// 并不会触发页面更新
info.a.b = 100;
// 以下方式会触发页面更新
// info.a = { b: 100 }
}
return {
info,
update1,
update2,
};
},
};
</script>
<template>
<button @click="update1">更新count</button>
<button @click="update2">更新b的值</button>
<div>{{ info.count }}</div>
<div>{{ info.a.b }}</div>
</template>
以上代码最终渲染效果如下:
# 7、readonly() 方法
TIP
接受一个对象 (不论是响应式还是普通的) 或是一个 ref (opens new window),返回一个原值的只读代理。
只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive()
相同,会自动解包,但解包得到的值是只读的。
要避免深层级的转换行为,请使用 shallowReadonly() (opens new window) 作替代。
<script>
import { reactive, readonly } from "vue";
export default {
setup() {
const proxyObj = reactive({ a: 1, b: { c: 2 } });
const readonlyObj = readonly(proxyObj);
// 对象属性可读
console.log(readonlyObj.a); // `
console.log(readonlyObj.b.c); // 2
// 以下对象属性赋值失败,同时在控制台抛出错误
readonlyObj.a = 100;
readonlyObj.b.c = 200;
},
};
</script>
# 8、isReactive() 方法
TIP
检查一个对象是否是由 reactive()
或 shallowReactive()
创建的代理。
返回值为
boolean
布尔类型值,true
表示是,false
表示否
<script>
import { reactive, isReactive, shallowReactive } from "vue";
export default {
setup() {
const obj = { a: 1 };
console.log(isReactive(obj)); // false
console.log(isReactive(reactive(obj))); // true
console.log(isReactive(shallowReactive(obj))); // true
},
};
</script>
# 四、深入响应式 API - 工具函数(二)
TIP
通过前面ref()
方法的学习,我们对 ref 对象有了初步的了解,本小节我们将深入展开ref
对象的学习。
本小节主要包含如下内容:
shallowRef()
方法,创建浅层的 ref 对象isRef()
方法,检查某个值是否为 ref 对象unref()
方法,获取 ref 属性值的一种语法糖customRef()
方法,自定义 ref
# 1、shallowRef() 方法
TIP
shallowRef()
方法相当于ref()
的浅层作用形式,只对.value
的访问是响应式的,对对象的其它属性值的写操作不支持响应式。
针对大型的数据结构,如果我们确实只需要浅层次的响应式,则可以利用shallowRef()
帮助我们提升性能,因为减小了深层次的递归操作。
<script>
import { ref, shallowRef } from "vue";
export default {
setup() {
const state = shallowRef({ count: 1 });
function update1() {
// 不会触发页面的更新
state.value.count = 100;
}
function update2() {
// 会触发页面的更新
state.value = { count: 200 };
}
return {
state,
update1,
update2,
};
},
};
</script>
<template>
<button @click="update1">更新1</button>
<button @click="update2">更新2</button>
<div>{{ state.count }}</div>
</template>
以上代码最终效果如下:
注:
点击更新 1 并不会触发页面的更新,只有点击更新 2 时,才会触发页面的更新。
因为 state 是被shallowRef
转换的的一个浅层的响应式对象,所以state.value.count = 100
并不会触发页面的更新,而state.value = { count: 200 }
会触发页面的更新。
# 2、isRef() 方法
TIP
检查某个值是否为 ref 对象,如果是返回 true,否则返回 false
import { isRef, ref } from "vue";
export default {
setup() {
console.log(isRef(ref(0))); // true
console.log(isRef(0)); // false
},
};
# 3、unref() 方法
TIP
unref()
方法的参数是 ref,则返回 ref 对象的 value 属性值,否则返回参数本身。
unref()
方法是val = isRef(val) ? val.value : val
计算的一个语法糖
import { isRef, unref, ref } from "vue";
export default {
setup() {
console.log(unref(ref("Hello"))); // Hello
console.log(unref("Hello")); // Hello
console.log(unref(ref({ a: 1, b: 2 }))); // Proxy(Object) {a: 1, b: 2}
console.log(unref({ a: 1, b: 2 })); // {a: 1, b: 2}
},
};
# 4、customRef() 自定义 ref
TIP
customRef()
方法用来创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
语法
let value=0;
// 返回值为ref对象
const refObj=customRef((track,trigger)=>{
// 返回一个对象
return {
get(){
// 获取ref的值时,会调用get方法
return value;
},
set(newValue){
// 修改ref的值时,会调用set方法
value=newValue
}
}
}
注:
为了帮助大家更好的理解customRef()
的用法,大家可以先按以下思路来创建一个自定义的 ref,实现数据与视图的同步更新。
第一步:创建 createRef()
函数
customRef()
方法的返回值为一个ref
,该方法接受一个回调函数作为参数。createRef(value)
方法直接将customRef()
方法的返回值返回,即返回值为 ref- 参数
value
为需要转换为ref
的原始值 - 以下代码中
count
为原始值 0 转换后的 ref 对象
// createRef为自定义创建ref的方法,接受一个参数value,value为需要转换为ref的原始值
function createRef(value) {
return customRef(() => {});
}
// 使用createRef将原始值 0 创建成 ref,即count为ref对象
const count = createRef(0);
第二步:customRef()
回调函数的返回值
customRef()
方法的回调函数返回一个带 get 和 set 的对象- 当访问 count 的值(
count.vaue
)时,会调用 get 方法获取到对应的值 - 当修改 count 的值(
count.value=100
)时,会调用 set 方法来更新对应的值
function createRef(value) {
return customRef(() => {
return {
// 当访问ref对象的value值时,会调用get方法
get() {
console.log(`get被调用了,此时返回的value值为:${value}`);
return value;
},
// 当给ref对象的value重新赋值时,会调用set方法
set(newValue) {
value = newValue;
console.log(`set被调用了,此时value的值更新为:${value}`);
},
};
});
}
// 使用createRef将原始值 0 创建成 ref,即count为ref对象
const count = createRef(0);
// 读取ref中的值
console.log(count.value);
// 更新ref的value值
count.value = 100;
第三步:在组件模板中使用 count
- 在组件模板中使用自定义的 ref 对象 count
- 定义 update 方法,当点击更新按扭时,调用该方法更新 count 的值
<script>
import { customRef } from "vue";
export default {
setup() {
function createRef(value) {
return customRef(() => {
return {
// 当访问ref对象的value值时,会调用get方法
get() {
console.log(`get被调用了,此时返回的value值为:${value}`);
return value;
},
// 当给ref对象的value重新赋值时,会调用set方法
set(newValue) {
value = newValue;
console.log(`set被调用了,此时value的值更新为:${value}`);
},
};
});
}
// 使用createRef将原始值 0 创建成 ref,即count为ref对象
const count = createRef(0);
// 读取ref中的值
console.log(count.value);
function update() {
// 更新ref的value值
count.value = 100;
}
// 对外暴露属性与方法
return {
count,
update,
};
},
};
</script>
<template>
<button @click="update">更新myRef的值</button>
<div>myRef的值:{{ count }}</div>
</template>
以上代码最终渲染效果如下:
分析执行结果
- 当我们点击对应的更新按扭更新
count
的值为 100 时,set 方法被调用了,但页面视图并没有同步更新。 - 因为我们并没有在get 中收集依赖(建立对应属性与相关 DOM 的依赖,让 Vue 知道数据更新后要更新那些 DOM)
- 也没有在set 方法中触发依赖(根据 get 方法中建立的依赖关系,触发 DOM 的更新)
第四步:customRef()
回调函数的track
与trigger
参数
customRef((track, trigger) => { })
回调函数的两个参数
track()
方法用来在 get 方法被调用时,收集依赖trigger()
方法用来在 set 方法被调用时,触发依赖
<script>
import { customRef } from "vue";
export default {
setup() {
function createRef(value) {
return customRef((track, trigger) => {
return {
// 当访问ref对象的value值时,会调用get方法
get() {
console.log(`get被调用了,此时返回的value值为:${value}`);
track();
return value;
},
// 当给ref对象的value重新赋值时,会调用set方法
set(newValue) {
value = newValue;
trigger();
console.log(`set被调用了,此时value的值更新为:${value}`);
},
};
});
}
// 使用createRef将原始值 0 创建成 ref,即count为ref对象
const count = createRef(0);
// 读取ref中的值
console.log(count.value);
function update() {
// 更新ref的value值
count.value = 100;
}
return {
count,
update,
};
},
};
</script>
<template>
<button @click="update">更新myRef的值</button>
<div>myRef的值:{{ count }}</div>
</template>
以上代码最终渲染效果如下:
# 4.1、总结:customRef() 方法使用流程
第一步:
- 创建一个自定义函数,该函数接受一个参数,这个参数为需要转换为 ref 的原始值
- 函数的返回值为
customRef()
方法的返回值 ref
function createRef(value) {
return customRef(() => {});
}
第二步:
- 创建 customRef 回调函数的返回值,返回值为一个带有 get 和 set 的对象。
- 当访问 ref 的 value 值时调用 get 方法,所有在此时需要处理的相关逻辑都可以写在 get 中
- 当修改 ref 的 value 值时调用 set 方法,所有在此时南要处理的相关链辑都可以写在 set 中
function createRef(value) {
return customRef(() => {
return {
get() {
return value;
},
set(newValue) {
value = newValue;
},
};
});
}
第三步:
- 在 get 方法返回值前调用
track()
方法,收集依赖 - 在 set 方法更新值之后调用
trigger()
方法,触发依赖
function createRef(value) {
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
value = newValue;
trigger();
},
};
});
}
通过前面的学习,我们知道如何创建自定义 ref,那自定义 ref 应该在什么场景下使用呢?
注:自定义 ref 使用场景
- 当我们在获取数据或更新数据时需要做一些额外的操作,就可以用自定义 ref。
- 比如:页面视图的更新并不需要同步更新,而是数据在 200ms 内如果没有连续更新,则再更新视图。
接下来,我们就来学习下 Vue 官方提供的《自定义防抖 ref》案例
# 5、实战应用:自定义防抖 ref
TIP
当在输入框中连续输入内容的间隔时间超过500ms
,则更新 h3 标签显示的内容,否则不更新。
如下:
<script>
import { customRef } from "vue";
export default {
setup() {
function useDebouncedRef(value, delay = 500) {
let timer = null; // 用来保存定时器
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
// 间隔时间不够500ms,则清除上一次定时器,重新计时
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger();
}, delay);
},
};
});
}
// 创建 ref
const text = useDebouncedRef("hello");
// 对外暴露text
return {
text,
};
},
};
</script>
<template>
<input v-model="text" />
<h3>{{ text }}</h3>
</template>
# 6、总结
shallowRef()
用来创建浅层的 ref 对象,只对.value
的访问是响应式的,对象的其它均为非响应式
const proxy = shallowRef({ a: 1 });
proxy.value = { a: 2 }; // 具有响应性,值被修改后页面视图会同步更新
proxy.value.a = 2; // 不具有响应性,值被修改后页面视图不会更新
isRef()
判断一个对象是不是 ref 对象,是返回 true,不是返回 false
console.log(isRef(0)); // false
console.log(isRef(ref(0))); // true
unref()
方法的参数是 ref,则返回 ref 对象的 value 属性值,否则返回参数本身。
const a = unref(ref(0)); // 0
const b = unref(0); // 0
const c = unref(reactive({ a: 1 })); // Proxy(Object){a:1}
customRef()
用来自定义 ref 对象,主要掌如何定义一个 ref 对象及实现自定义防抖 ref
// value为ref对象value属性的原始值
function createRef(value) {
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
value = newValue;
trigger();
},
};
});
}
// 定义一个ref对象
const count = createRef(0);
# 五、setup() 函数参数
TIP
深入浅出 setup() 函数参数 props、context、什么情况下会使用 setup() 函数
# 1、参数 props
TIP
setup()
函数有两个参数,第一个参数是组件的props
,和标准的组件一致。- 一个
setup
函数的props
是响应式的,并且会在传入新的props
时同步更新。
export default {
props: ["list", "title"],
setup(props) {
console.log(props.title); // 访问传递的title属性
console.log(props.list); // 访问传递的list属性
},
};
代码示例
App.vue
<script>
import { reactive, ref } from "vue";
import List from "./components/List.vue";
export default {
setup() {
const list = reactive(["新闻1..", "新闻2..", "新闻3.."]);
const title = ref("新闻标题");
// 更新方法
function update() {
(title.value = "更新后--标题"), list.push("新加内容一条");
}
return {
list,
title,
update,
};
},
components: {
List,
},
};
</script>
<template>
<List :list="list" :title="title" @event-update="update"></List>
</template>
List.vue
<script>
export default {
props: ["list", "title"],
emits: ["eventUpdate"],
setup(props) {
setTimeout(() => {
console.log(props.title); // 访问传递的title属性
console.log(props.list); // 访问传递的list属性
}, 3000);
},
};
</script>
<template>
<button @click="$emit('eventUpdate')">更新</button>
<h3>{{ title }}</h3>
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</template>
最终渲染效果如下:
点击更新按扭更新 props 的内容,
3s
后,setup()
函数中的 props 的值也更新为了最新的。
# 1.1、解构 props
TIP
通过上面案例我们知道 props 参数的值为一个响应式代理对象,但如果直接解构props
部分属性将失去响应性。因此推荐通过props.xxx
的形式来使用 props
- 如果我们确实需要解构
props
,但解构后对应变量仍需保持响应性,可以利用toRefs()
工具函数来实现
setup(props) {
let { title, list } = toRefs(props)
}
- 如果我们需要将 props 中的某一个 prop 传递到外部,并保持其响应性,则可以利用
toRef()
工具函数来实现
setup(props) {
let title = toRef(props, 'title')
}
# 2、参数 context
TIP
传入 setup
函数的第二个参数 context 是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup
中可能会用到的值。
如下:
setup(props, context) {
// context是非响应式的普通对象
console.log("context", context)
// 透传 Attributes(非响应式的对象(只读),等价于 $attrs)
console.log("attrs", context.attrs)
// 插槽(非响应式的对象,等价于 $slots)
console.log("slots", context.slots)
// 触发事件(函数)等价于 $emit)
console.log("emit", context.emit)
// 暴露组件公共属性(函数)
console.log("expose", context.expose)
}
以下为上面代码在某个案例中的打印结果
代码示例:
- App.vue
<script>
import { reactive, ref } from "vue";
import List from "./components/List.vue";
export default {
setup() {
const title = ref("新闻标题");
let list = reactive([1, 2, 3]);
// 更新
function update() {
title.value = "最新动态";
}
return {
title,
list,
update,
};
},
mounted() {
// 子组件List对外暴露了num属性,所以可以访问到
console.log("num", this.$refs.myList.num); // 100
// 子组件List并没有对外暴露count属性
console.log("count", this.$refs.myList.count); // undefined
},
components: {
List,
},
};
</script>
<template>
<List :title="title" @update-event="update" class="list" ref="myList">
<!--默认插槽内容-->
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
<!--具名插槽内容-->
<template v-slot:footer>
<a href="#">...点击查看更多</a>
</template>
</List>
</template>
- List.vue
<script>
import { ref } from "vue";
export default {
props: ["title"],
emits: ["updateEvent"],
setup(props, context) {
// context是非响应式的普通对象
console.log("context", context);
// 透传 Attributes(非响应式的对象(只读),等价于 $attrs)
console.log("attrs", context.attrs);
// 插槽(非响应式的对象,等价于 $slots)
console.log("slots", context.slots);
// 触发事件(函数)等价于 $emit)
console.log("emit", context.emit);
// 暴露组件公共属性(函数)
console.log("expose", context.expose);
// 暴露组件公共属性
const count = ref(100);
const num = ref(2000);
context.expose({ count });
},
};
</script>
<template>
<div>
<h3>{{ title }}</h3>
<!--默认插槽-->
<slot>显未主体内容</slot>
<div class="footer">
<!--具名插槽-->
<slot name="footer"></slot>
</div>
</div>
</template>
通过上图可以看到,
context
上下文对象是非响应式的,可以安全地解构
export default {
// props是响应式的
// attrs,slots非响应式(只读的) emit、expose为函数
setup(props, { attrs, slots, emit, expose }) {
...
}
}
attrs
和slots
都是有状态的对象,它们总是会随着组件自身的更新而更新- 这意味着你应当避免解构它们,并始终通过
attrs.x
或slots.x
的形式使用其中的属性
/* 不应该解构他们 */
setup(props, { attrs, slots, emit, expose }) {
const {xx,xxx}=attrs;
const {xx,xxx}=slots
}
# 3、什么情况下会使用 setup() 函数
TIP
通过前面的学习,我们知道组合式 API 可以与选项式 API 共存,也就是说,我们可以在选项式 API 中使用setup()
函数。
但在实际开发中,我们并不推荐两种方式混合使用,更希望在项目中选择其中的一种方式来开发。但在以下情况,选项式 API 会与组合式 API 共存。
- 旧项目改造: 我们在改造旧项目时,旧项目使用的是选项式 API 书写的,但是我们现在想在原来的基础上集成基于组合式 API 的代码。
- 非单文件组件: 在非单文件组件中我们想要使用组合使 API 时,就必需要使用到
setup()
函数。
// App.js
import { ref } from "https://unpkg.com/vue@3/dist/vue.esm-browser.js";
export default {
setup() {
const msg = ref("Hello Vue!!");
return {
msg,
};
},
template: `<div>{{msg}}</div>`,
};
温馨提示
对于单文件组件开发中想要使用组合式 API,我们更推荐在<script setup>....</script>
标签中来书写,代码更加简加及符合程序员编写代码的习惯。
在本章的第三个版块《单文件组件中使用组合式 API》中会讲到
<script setup>
大厂最新技术学习分享群
微信扫一扫进群,获取资料
X