Vue3新语法
1、组合式API的了解
Vue3除了沿用传统的选项式API的代码模式,还引入了组合式API,它允许开发人员以更好的方式编写代码。使用组合式API,开发人员可以将逻辑代码块组合在一起,从而编写出可读性高的代码。
回顾一下选项式API的代码书写方式,其实选项式API暴露的最主要的一个问题是:操作同一内容目标的代码会被分散在不同的选项中,如data、methods、computed、watch及生命周期钩子函数等。
来看下方代码,如果我们想要操作的只有count这一个数据对象,那么选项式API的代码看起来会十分凌乱。
来看下方代码,如果我们想要操作的只有count这一个数据对象,那么选项式API的代码看起来会十分凌乱。
html
<div id="app">
<p>count:{{count}}</p>
<p>double:{{double}}</p>
<button @click="increase">increase</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const vm = Vue.createApp({
//响应式数据定义
data() {
return {
count: 0
}
},
methods: {
increase() {
this.count++;
}
},
computed: {
double() {
return this.count * 2;
}
},
watch: {
count(newVal, oldVal) {
console.log(newVal, oldVal);
}
}
}).mount('#app');
</script>

使用组合式API可以将代码组织成更小的逻辑片段,并将它们组合在一起,甚至在后续需要重用它们时可以进行抽离。
现在利用组合式API对上方代码进行重写,可以看到操作count数据对象的所有功能代码都被集中到一起,便于代码的编写、查看及后续维护。
html
<div id="app">
<p>count:{{count}}</p>
<p>double:{{double}}</p>
<button @click="increase">increase</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {ref, computed, watch} = Vue;
const vm = Vue.createApp({
setup(props, context) {
//初始化ref响应式数据
const count = ref(0);
//定义更新数据的函数
const increase = ()=> {
count.value++;
};
//定义计算属性
const double = computed(()=> count.value * 2);
//定义watch函数,监听count值的变化
watch(count, (newVal, oldVal)=> {
console.log(newVal, oldVal);
});
//返回响应式数据以及方法与计算属性
return {
count,
increase,
double
};
}
}).mount('#app');
</script>
上方代码只是对组合式API进行了简单展示,其实组合式API的优点远不止于此。除了更灵活的代码组织、更好的逻辑重用,组合式API还提供了更好的TypeScript类型接口支持,Vue3本身也是使用TypeScript编写实现的,为了提升性能还实现了treeShaking(treeShaking直译为摇树,是一种消除死代码的性能优化理论,比如,现在想引入lodash第三方类库中的方法,但不做全部引入,只是引入单个方法,此时其他的方法都不会被打包处理,程序只会将其单个方法的代码抽离),从而产生更小的生产包和更少的网络开销。
2、setup组合式API入口函数
Vue3既能使用选项式API又能使用组合式API,那么应该如何区分代码方式呢?其实很好区分,Vue3为组合式API提供了一个setup函数,所有组合式API函数都是在此函数中调用的,它是组合式API的使用入口。setup函数接收两个参数:
- 第1个参数是props对象,包含传入组件的属性;
- 第2个参数是context上下文对象,包含attrs、emit、slot等对象属性。
在使用组合式API定义响应式数据之前,有两个点需要我们重点关注:
- 一个是setup函数必须返回一个对象,在模板中可以直接读取对象中的属性,以及调用对象中的函数;
- 另一个是setup函数中的this在严格模式下是undefined,不能像选项式API那样通过this来进行响应式数据的相关操作;
请思考下面代码的运行效果。
html
<div id="app">
<p>msg:{{msg}}</p>
<button @click="handleClick">测试</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
'use strict';
const vm = Vue.createApp({
setup(props, context) {
console.log(this);//this是undefined
let msg = 'Hello atguigu!';
function handleClick() {
alert('响应点击');
msg += '--';
}
//返回对象中的属性和函数,在模板中可以直接使用
return {
msg,
handleClick
}
}
}).mount('#app');
</script>
输出的this是undefined,对象中的msg属性值在模板中直接显示在页面上。点击"测试"按钮后,handleClick函数就会自动调用,从而显示警告提示,如图所示。

但是这里要注意,我们在按钮的点击回调函数handleClick中更新了msg,而页面并不会自动更新。这是因为msg只是一个普通的字符串,并不是一个响应式数据。如何定义响应式数据呢?Vue3为开发者提供了ref和reactive等函数。
3、利用ref函数定义响应式数据
ref是Vue3组合式API中常见的用来定义响应式数据的函数。ref函数接收一个任意类型的数据参数作为响应式数据,由Vue内部保存。ref函数返回一个响应式的ref对象,通过ref对象的value属性可以读取或者更新内部保存的数据。
要想让模板操作ref对象,需要将ref对象添加到setup函数返回的对象中。由于ref对象是响应式的,因此在模板中操作ref对象比较特殊,不需要我们亲自添加.value去操作它内部的数据,只需要指定ref对象就可以实现操作,因为模板在编译时会自动添加.value来读取或更新value属性值。
将上面所述过程通过代码来演示,具体如下。
html
<div id="app">
<!-- 在模板中不用添加.value,内部模板在编译时会自动添加-->
<p>count:{{count}}</p>
<button @click="increaseCount">增加</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {ref} = Vue;
const vm = Vue.createApp({
setup(props, context) {
//通过调用ref函数产生ref对象count,并指定内部保存的数据的初始值为0
const count = ref(0);
//读取ref对象中的value属性值
console.log(count.value);//0
//定义更新ref响应式数据的函数
const increaseCount = ()=> {
//先读取value属性值,加1后再更新到value属性上
count.value = count.value + 1;
};
//返回包含ref对象和更新函数的对象
return {
count,
increaseCount
}
}
}).mount('#app');
</script>
运行代码后,页面上显示的是count的初始值0;点击"增加count"按钮后,显示的数量会自动增加1,如图所示。

ref函数除了可以接收基础类型的数据,还可以接收对象或数组类型的数据。无论是在JavaScript代码中,还是在模板代码中,我们都可以进行读取或更新数据操作。需要注意的是,只有在JavaScript代码中才能通过添加.value来操作,而在模板中则不能通过添加.value来操作,请思考下方代码。
html
<div id="app">
<!-- 在模板中不用添加.value,内部模板在编译时会自动添加 -->
<p>person:{{person}}</p>
<button @click="setNewPerson">指定新的人</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {ref} = Vue;
const vm = Vue.createApp({
setup(props, context) {
//创建ref对象,并指定内部初始值为一个人员信息
const person = ref({
name: 'Tom',
age: 12
});
function setNewPerson() {
//执行value为一个新的人员信息
person.value = {name: 'Jack', age: 23};
}
//返回包含ref对象和更新函数的对象
return {
person,
setNewPerson
}
}
}).mount('#app');
</script>
运行代码后,页面上显示的是Tom的信息。点击"指定新的人"按钮后,就变为了Jack的信息,如图所示。


数组和对象的读取与更新采用的是相同的方式,这里不多做讲解。如果点击按钮不是指定一个新的人员信息,而是更新对象中的name或age属性值(如person.value.age=24),那么页面会自动更新吗?答案是会更新,但这涉及reactive函数,后面会具体讲解。
4、利用reactive函数定义响应式数据
利用ref函数专门来定义包含单个数据的响应式对象的方法,那么在应用中如果需要定义包含多个数据的响应式对象该怎么实现呢?Vue3提供了reactive函数,让开发者可以一次性定义包含多个数据的响应式对象。
reactive函数接收一个包含n个基础类型或对象类型属性数据的对象参数,它会返回一个响应式的代理对象,一般我们称此对象为"reactive对象"。在JavaScript或模板中可以通过reactive对象直接读取或更新参数对象中的任意属性数据。需要强调一点,reactive函数进行的是一个深度响应式处理。也就是说,当我们通过reactive对象更新参数对象中的任意层级属性数据后,都会触发页面的自动更新。
html
<div id="app">
<ul>
<li>msg:{{state.msg}}</li>
<li>person:{{state.person}}</li>
<li>courses:{{state.courses}}</li>
</ul>
<button @click="updateMsg">更新msg</button>
<button @click="updatePerson">更新person</button>
<button @click="updateCourses">更新courses</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {reactive} = Vue;
const vm = Vue.createApp({
setup(props, context) {
// 使用reactive函数定义包含多个数据的响应式对象
const state = reactive({
msg: 'Hello Atguigu', //基础类型
person: {name: 'Tom', age: 22},//对象类型
courses: ['JavaScript', 'BOM', 'DOM'] //数组类型
});
//更新字符串msg的函数
const updateMsg = ()=> {
state.msg += '--';
};
//更新对象person的函数
const updatePerson = ()=> {
state.person = {name: 'Jack', age: 33};
};
//更新数组courses的函数
const updateCourses = ()=> {
state.courses = ['JavaScript2', 'BOM2', 'DOM2'];
};
return {
state,
updateMsg,
updatePerson,
updateCourses
};
}
}).mount('#app');
</script>
上方代码先使用reactive函数定义了包含基础类型、对象类型和数组类型的3个属性数据的响应式对象state,然后分别定义了更新字符串msg、更新对象person和更新数组courses的3个函数,最后将state对象和这3个函数都返回模板中使用。在模板中通过state对象来读取内部的3个对象进行动态显示,同时将返回的3个函数分别绑定到3个按钮的点击事件上。

依次点击"更新msg""更新person""更新courses"按钮,页面会自动更新显示,如图所示。

前面提过,reactive函数进行的是深度响应式处理,结合刚才的person属性来说,不仅直接更新person对象会触发页面更新,更新person对象中的任意属性(name或age)也会触发页面更新,甚至我们给person对象添加一个新属性或删除已有属性,页面同样会更新。将updatePerson函数修改为下方代码。
js
const updatePerson = ()=> {
//指定新的person对象
state.person = {name: 'Jack', age: 33};
//更新对象中的已有属性
state.person.name += '==';
state.person.age += 2;
//给对象添加新属性
state.person.sex = '男';
};
此时点击"更新person"按钮,这3种更新方式都能触发页面更新。需要注意的是,直接添加新属性和删除已有属性在Vue2中是不可以触发页面更新的,但在Vue3中是可以的。

reactive函数定义的数组数据同样进行的是深度响应式处理,我们不仅可以通过调用数组方法进行响应式更新,还可以通过下标进行响应式更新。将updateCourses函数修改为下方代码。
js
//更新数组courses的函数
const updateCourses = ()=> {
//指定新的数组
state.courses = ['JavaScript2', 'BOM2', 'DOM2'];
//通过调用数组方法更新数组元素
state.courses.splice(1, 1, 'Vue2');
//通过下标更新数组元素
state.courses[2] = 'Vue3';
};
点击"更新courses"按钮,这两种方式都能触发页面更新。需要注意的是,在Vue2中直接通过下标更新数组元素不可以触发页面更新,但在Vue3中是可以的。

先给ref函数传入一个对象或数组数据,再通过ref对象来操作对象或数组的内部数据,发现也是响应式的,这是为什么呢?因为一旦ref函数内部的value数据是对象或数组,就会自动先创建一个包含此对象或数组的reactive对象,也就是代理对象,再保存给ref对象的value,比如下方代码。
js
const person = ref({
name: 'Tom', age: 12
});
person.value.age = 13;
更新age属性就是一个响应式的数据更新,因为person.value是ref函数接收的人员信息的reactive对象,也就是代理对象,通过reactive对象去更新对象的内部属性,必然是一个响应式的数据更新,页面自然会更新。
5、toRefs与toRef函数
在介绍toRefs与toRef函数之前,先来演示使用reactive函数进行代码简化的问题,下面是简化前的代码。
html
<div id="app">
<ul>
<li>msg:{{state.msg}}</li>
<li>person:{{state.person}}</li>
<li>courses:{{state.courses}}</li>
</ul>
<button @click="updateState">更新state</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {reactive, toRef, toRefs} = Vue;
const vm = Vue.createApp({
setup(props, context) {
const state = reactive({
msg: 'Hello Atguigu', //基础类型
person: {name: 'Tom', age: 22},//对象类型
courses: ['JavaScript', 'BOM', 'DOM'] //数组类型
});
const updateState = ()=> {
state.msg += '--';
state.person = {name: 'Jack', age: 33};
state.courses = ['JavaScript2', 'BOM2', 'DOM2'];
};
return {
state,
updateState
};
}
}).mount('#app');
</script>
在模板中,每次获取reactive函数中的数据都要写上"state.",如果需要获取多次,则操作会重复多次。现在想简化一下代码,去掉"state.",也就是像下面这样编写代码。
html
<ul>
<li>msg:{{msg}}</li>
<li>person:{{person}}</li>
<li>courses:{{courses}}</li>
</ul>
或者利用扩展运算符简化代码。
js
return {
...state,
updateMsg
}
但是,运行代码并点击按钮更新msg后,发现页面上的msg显示不会更新。这是因为后面的两种方式传入的msg、person和course属性值都是非响应式数据,而要想页面能自动更新,必须要求setup函数返回对象中的属性是ref对象或reactive对象。
如何让从reactive对象中读取出的属性也是响应式的呢?
答案是:可以利用toRefs和toRef函数。
- toRefs函数能一次性将reactive对象包含的所有属性值都包装成ref对象
- toRef函数只能一次处理一个属性。
详细来说,toRefs函数接收一个reactive对象,内部会包装每个属性值生成ref对象,最后返回包含所有属性值的ref对象的对象。而toRef函数接收reactive对象和属性名,内部会创建此属性名对应属性值的ref对象,并返回这个ref对象。
5.1、toRefs
一般我们会使用toRefs函数来解决上面的问题,代码如下。
js
//生成包含多个ref对象的对象
const stateRefs = toRefs(state);
const updateState = ()=> {
state.msg += '--';
state.person = {name: 'Jack', age: 33};
state.courses = ['JavaScript2', 'BOM2', 'DOM2'];
};
return {
msg: stateRefs.msg,
person: stateRefs.person,
courses: stateRefs.courses,
updateState
};
当然可以进一步简化代码,直接用扩展运算符解构toRefs函数生成的对象。
js
return {
...toRefs(state),
updateState
};
5.2、toRef
那么如果使用toRef函数来处理,要如何编写代码呢?可以先多次调用toRef函数生成各个属性的ref对象,再添加到返回对象中,代码如下。
js
//生成各个属性的ref对象
const msgRef = toRef(state, 'msg');
const personRef = toRef(state, 'person');
const coursesRef = toRef(state, 'courses');
return {
msg: msgRef,
person: personRef,
courses: coursesRef,
updateState
};
当然可以进一步优化成如下代码。
js
return {
msg: toRef(state, 'msg'),
person: toRef(state, 'person'),
courses: toRef(state, 'courses'),
updateState
};
虽然使用toRefs和toRef函数都能解决解构reactive对象的属性读取响应式丢失的问题,但从代码可读性上来看,明显使用toRefs函数的代码更简洁。toRef函数更适用于只对单个属性进行处理的场景,而toRefs函数更适用于对所有属性进行处理的场景。
6、readonly与shallowReadonly函数
无论是reactive对象(也称为代理对象)还是ref对象,进行的都是深度响应式处理。也就是说,我们通过reactive对象或ref对象进行属性的深度读取和修改操作,修改后能触发页面的自动更新。而如果我们想产生一个只包含读取能力的reactive对象,就可以使用readonly与shallowReadonly函数。
readonly和shallowReadonly函数接收的参数是一样的,其可以是一个原始的非响应式对象,也可以是响应式的reactive对象。
- 只是readonly函数产生的reactive对象是深度只读的,
- 而shallowReadonly函数产生的reactive对象只有外层属性是只读的,所有嵌套的内部属性都是可读/写的。
下面我们以接收一个reactive对象为例来演示readonly与shallowReadonly函数的使用方法,代码如下。
html
<div id="app">
<h3>reactive对象显示</h3>
<p>person.name:{{person.name}}</p>
<p>person.addr.city:{{person.addr.city}}</p>
<h3>readonly对象显示</h3>
<p>rPerson.name:{{rPerson.name}}</p>
<p>rPerson.addr.city:{{rPerson.addr.city}}</p>
<h3>shallowReadonly对象显示</h3>
<p>srPerson.name:{{srPerson.name}}</p>
<p>srPerson.addr.city:{{srPerson.addr.city}}</p>
<button @click="update1">通过reactive对象更新</button>
<button @click="update2">通过readonly对象更新</button>
<button @click="update3">通过shallowReadonly对象更新</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {reactive, readonly, shallowReadonly} = Vue;
const vm = Vue.createApp({
setup(props, context) {
//产生深度可读/写的reactive对象
const person = reactive({
name: '张三',
addr: {
city: '北京',
}
});
//产生深度只读的reactive对象
const rPerson = readonly(person);
//产生浅只读的reactive对象
const srPerson = shallowReadonly(person);
const update1 = ()=> {
person.name += '--';//控制台没有告警提示,页面会自动更新
person.addr.city += '++';//控制台没有告警提示,页面会自动更新
};
const update2 = ()=> {
rPerson.name += '--';//控制台给出告警提示,且页面不会自动更新
rPerson.addr.city += '++';//控制台给出告警提示,且页面不会自动更新
};
const update3 = ()=> {
srPerson.name += '--';//控制台给出告警提示,且页面不会自动更新
srPerson.addr.city += '++';//控制台没有告警提示,页面会自动更新
};
return {
person,
rPerson,
srPerson,
update1,
update2,
update3
}
}
}).mount('#app');
</script>
代码运行后的页面效果如图所示。

当点击"通过reactive对象更新"按钮时,在回调中通过reactive对象更新外部属性name或内部属性city,程序是正常运行的,且页面会自动更新;

当点击"通过readonly对象更新"按钮时,在回调中通过readonly函数产生的深度只读reactive对象来更新外部属性name或内部属性city,控制台会给出警告提示,且页面不会自动更新;

当点击"通过shallowReadonly对象更新"按钮时,在回调中通过shallowReadonly函数产生浅只读reactive对象,如果更新外部属性name,则控制台会给出警告提示,且页面不会自动更新,如果更新内部属性city,则程序正常运行,且页面会自动更新。

同时Vue3提供了用来判断是否是只读reactive对象的工具函数isReadonly,如果是通过readonly或shallowReadonly函数产生的只读对象,则isReadonly函数返回true,否则返回false,验证如下。
js
console.log(isReadonly(person));//false
console.log(isReadonly(rPerson));//true
console.log(isReadonly(srPerson));//true
那么readonly与shallowReadonly函数进行只读对象转化操作的应用场景是什么呢?试想,如果在项目开发过程中团队成员开发了一些功能组件,想要实现其他成员可以使用对应组件,但不能修改其数据,也就是对功能进行一定的约束,就可以通过readonly与shallowReadonly函数将数据进行只读转化,这样既可以保证程序的高度可读性,也可以保证数据的安全性。
7、shallowRef与shallowReactive函数
ref和reactive函数都会对数据进行深度响应式处理。也就是说,我们通过ref对象更新value属性,或者更新value属性对象中的嵌套属性,页面都会自动更新。通过reactive对象更新目标对象中的属性,或者更新目标对象中属性对象的嵌套属性,页面都会自动更新。如果我们只想进行外部属性的响应式处理,不想进行嵌套属性的响应式处理,则可以使用Vue3提供的shallowRef与shallowReactive函数来实现。
-
shallowRef函数是ref函数的浅层实现,接收一个原始对象,返回一个ref对象。ref对象内部保存的value就是传入的原始对象,而不是一个响应式的代理对象。这就意味着更新value是响应式的,但更新value对象内部的属性不是响应式的。
-
shallowReactive函数是reactive函数的浅层实现,接收一个原始对象,返回一个代理对象(也称为reactive对象)。对于原始对象中嵌套的属性对象,shallowReactive函数并没有创建对应的代理对象,也就是说,属性对象不是响应式的,更新属性对象中的属性,页面不会自动更新。
下面我们通过代码来演示一下shallowRef与shallowReactive函数的使用方法。
html
<div id="app">
<h3>课程信息</h3>
<ul>
<li>名称:{{course.title}}</li>
<li>天数:{{course.days}}</li>
</ul>
<h3>人员信息:</h3>
<ul>
<li>姓名:{{person.name}}</li>
<li>城市:{{person.addr.city}}</li>
</ul>
<button @click="update1">浅更新ref对象</button>
<button @click="update2">浅更新reactive对象</button>
<button @click="update3">深更新ref对象</button>
<button @click="update4">深更新reactive对象</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {reactive, shallowRef, shallowReactive} = Vue;
const vm = Vue.createApp({
setup(props) {
//定义浅ref对象
const course = shallowRef({
title: '',
days: 0
});
console.log(course);
//定义浅reactive对象
const person = shallowReactive({
name: '',
addr: {
city: ''
},
});
console.log(person);
//直接更新value属性,页面会自动更新
const update1 = ()=> {
course.value = {
//页面会自动更新
title: 'Vue技术栈',
days: 40
};
};
//更新外部属性,页面会自动更新
const update2 = ()=> {
person.name = '李四';
person.addr = {
city: '北京',
};
};
//更新value属性对象的内部属性,页面不会自动更新
const update3 = ()=> {
course.value.title = 'React技术栈';
course.value.days = 20;
};
//更新深层的属性,页面不会自动更新
const update4 = ()=> {
person.addr.city = '上海';
};
return {
course,
person,
update1,
update2,
update3,
update4
}
}
}).mount('#app');
</script>
我们在代码中通过shallowRef函数定义了一个浅ref对象course,通过shallowReactive函数定义了一个浅reactive对象person,后面在按钮的点击回调中分别对这两个响应式对象的数据进行浅属性更新和深度属性更新,看看页面是否会自动更新。
代码运行后的页面效果如图所示。

点击"浅更新ref对象"按钮,直接更新浅ref对象course的value属性,页面会自动更新。

点击"浅更新reactive对象"按钮,通过浅reactive对象person更新目标对象中的外部属性name和addr,页面会自动更新。

点击"深度更新ref对象"按钮,更新course对象的value属性对象上的title和days属性,而外部的value属性并没有发生变化,因此页面不会自动更新。

点击"深度更新reactive对象"按钮,通过person对象更新addr属性对象中的city属性,这是一个嵌套属性更新,外部的addr属性并没有发生变化,因此页面不会自动更新。

那么在什么场景下使用shallowRef与shallowReactive函数呢?如果页面中需要动态显示一个包含嵌套对象的对象或数组,就需要将其定义成响应式数据。当然我们可以选择使用ref或reactive函数实现,但如果嵌套对象中的属性并不需要单独更新,则可以选择使用shallowRef或shallowReactive函数实现。由于shallowRef和shallowReactive函数只做了外部(单层)的响应式处理,因此其相比ref和reactive函数来说内存占用更小,处理性能也更高。
8、toRaw与markRaw函数
如果我们想得到一个reactive对象内部包含的原始对象,就可以选择使用toRaw函数。如果我们不想让一个原始对象包装生成reactive响应式对象,就可以选择使用markRaw函数。
- toRaw函数接收的参数为一个reactive对象,返回值为内部包含的整个原始对象。
- markRaw函数接收的参数为一个原始对象,返回值还是这个原始对象,但其被添加了不能转换为reactive对象的标识属性
__v_skip,该值为true。当我们将这个对象传入reactive函数时,返回的就不是代理对象了,而是参数对象本身。也就是说,它并不是响应式对象。
下面通过代码来演示toRaw与markRaw函数的使用方法。
html
<div id="app">
<p>state.name:{{state.name}}</p>
<p>state.addr.city:{{state.addr.city}}</p>
<button @click="test1">测试roRaw</button>
<hr>
<p>state2.name:{{state2.name}}</p>
<p>state2.addr.city:{{state2.addr.city}}</p>
<button @click="test2">测试markRaw</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {reactive, toRaw, markRaw} = Vue;
const vm = Vue.createApp({
setup(props) {
//定义reactive对象
const state = reactive({
name: '张三',
addr: {
city: '北京'
}
});
//测试使用toRaw函数
const test1 = ()=> {
//通过toRaw函数得到reactive对象中包含的原始对象
const rawPerson = toRaw(state);
console.log(rawPerson);
};
//原始对象
const person2 = {
name: '李四',
addr: {
city: '上海'
}
};
//标记一个原始对象不能生成reactive对象,并返回这个对象
const markPreson2 = markRaw(person2);
console.log(markPreson2);//多了一个__v---skip为true的属性
//对markRaw函数的对象进行reactive处理,会被原样返回,它不是响应式的
const state2 = reactive(markPreson2);
console.log(state2);
console.log(markPreson2 === person2, state2 === markPreson2);
//测试更新markRaw函数的reactive对象,页面不会自动更新
const test2 = ()=> {
state2.name += '--';
state2.addr.city += '--';
console.log(state2.name, state2.addr.city);
};
return {
state,
state2,
test1,
test2
}
}
}).mount('#app');
</script>
在上面的代码中先定义了一个reactive对象state,然后在"测试toRaw"按钮的点击回调中,调用toRaw函数,并传入state对象,得到的就是state对象中包含的原始对象。运行代码后的页面效果如图所示。
接着定义了一个原始对象person2,调用markRaw函数,并传入person2对象,返回的还是person2对象。只是对象被添加了__v_skip为true的属性,该属性用来标识此对象不能生成reactive对象。控制台输出如图所示。

随后调用reactive函数,传入带__v_skip属性的对象,就不再返回一个代理对象,而是返回传入的对象本身,这就意味着此对象不能再进行响应式处理。

我们在setup函数的返回对象中添加了state2,模板可以通过state2读取里面任意层级的属性进行显示。但在"测试markRaw"按钮的点击回调test2中,通过state2来更新内部属性数据,页面不会自动更新,但通过控制台打印输出可以发现数据确实已经变了,如图3-16所示。这也就说明了,被markRaw函数处理的对象,不能生成响应式的reactive对象。

那么,在什么情况下需要使用toRaw与markRaw函数呢?当我们想得到reactive对象中包含的整个原始对象时,toRaw函数就是一个不错的选择。当我们为其他模块提供一个包含多个数据的对象时,如果我们不需要,也不希望外部使用者将其处理为响应式对象,就可以先使用markRaw函数来处理这个对象后再返回它。
9、computed函数
Vue3的组合式API中的computed函数与选项式API中的computed属性的功能是一样的,只是在语法的使用上不同而已,但不管是在组合式API还是在选项式API中,都是既可以只指定getter,也可以指定getter(指get方法)和setter(指set方法)的。
computed函数接收的参数可以是一个函数,也可以是一个包含get方法和set方法的对象。
- 当参数为函数时,就是指定了getter,用来返回动态计算的结果值;
- 当参数为对象时,就是指定了getter和setter,此时既可以通过getter返回动态计算的结果值,也可以通过setter监听计算属性的修改。
computed函数的返回值是计算属性对象,它本质上是一个ref对象,getter返回的值会被自动保存到其value属性上。当我们修改计算属性对象的value属性值时,setter就会被自动调用。
下面通过代码演示computed函数的使用方法。
html
<div id="app">
<h3>测试只带get方法的计算属性</h3>
<p>数量:{{count}}</p>
<p>姓名:{{person.name}}</p>
<p>信息(get方法计算属性):{{info}}</p>
<button @click="update">更新</button>
<h3>测试带get和set方法的计算属性</h3>
<input type="text" v-model="doubleCount">
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {reactive, ref, computed} = Vue;
const vm = Vue.createApp({
setup(props) {
//定义ref对象
const count = ref(0);
//定义reactive对象
const person = reactive({
name: '张三'
});
//定义只有getter的计算属性
const info = computed(()=>{
return `${person.name}完成的数量${count.value}`;
});
//定义有getter和setter的计算属性
const doubleCount = computed({
get() {
console.log(info);
return count.value *2;
},
set(value) {
count.value = value / 2;
}
});
//更新ref或reactive数据
const update = ()=>{
count.value += 1;
person.name += '--';
};
return {
count,
person,
info,
update,
doubleCount
}
}
}).mount('#app');
</script>
代码运行后的页面效果如图所示。

在上面的代码中,我们先分别定义了一个ref对象count和一个reactive对象person,然后调用computed函数来定义计算属性info,传入的参数为用于动态计算属性值的get函数。computed函数返回的info对象,本质上是一个ref对象,可以通过控制台进行查看,如图所示。

根据控制台输出的结果可以看出,内部"_value"的值就是执行get函数,在读取前面的ref和reactive响应式数据后,计算并返回的结果。同时返回了计算属性对象,在模板中就可以读取计算属性显示。
当点击"更新"按钮时,我们在点击回调中更新了计算属性依赖的ref数据和reactive数据,info计算属性的get函数会自动执行,并将返回的结果值更新到页面上。

当然如果模板中有多处读取计算属性,则计算属性的get函数只会执行一次,因为计算属性有缓存,这是与选项式API一样的特点。
接着定义了带get和set方法的计算属性doubleCount,在get方法中返回前面count对象的两倍值,在set方法中将最新值的一半更新给了count对象。在返回doubleCount计算属性的同时,在模板中利用v-model指令对doubleCount计算属性进行双向数据保存处理。
在初始显示时,程序会自动执行doubleCount计算属性的get方法,根据初始count值进行计算,返回0并显示到输入框中。当用户点击"更新"按钮更新count值时,doubleCount计算属性的get方法会被自动调用,返回新计算结果并显示到输入框中;当用户在输入框中改变输入内容时,v-model指令会将输入的最新值赋给doubleCount计算属性,这自然会触发set方法执行。随后在set方法中更新了count值,这样页面上的数量和计算属性信息都会更新显示。

10、watch函数
Vue3提供的组合式API函数watch,从功能上看,与选项式API的watch配置和$watch方法相同,但在语法使用上还是有些许差别的。
watch函数可以监听一个或多个响应式数据,当响应式数据发生变化时,监听的回调就会自动执行。
watch函数接收3个参数,具体如下:
- 第1个参数是被监听的一个或多个响应式数据,该参数有3种形式,具体如下。
- 一个reactive对象或ref对象。
- 返回reactive对象中基础类型属性的函数。
- 包含任意多个reactive对象、ref对象或函数的数组。
- 第2个参数是监听回调函数,该回调函数可接收两个参数,具体如下。
- 一个新值或包含多个新值的数组。
- 一个旧值或包含多个旧值的数组。
- 第3个参数是可选的配置对象,包含是否立即执行的immediate和是否深度监听的deep。
html
<div id="app">
<p>count:{{countObj.count}}</p>
<p>person:{{person.name}}-{{person.addr.city}}</p>
<button @click="update">更新</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {reactive, ref, watch} = Vue;
const vm = Vue.createApp({
setup(props) {
const countObj = ref({
count: 0,
});
const person = reactive({
name: '张三',
addr: {
city: '北京'
}
});
const update = ()=> {
//准备更新ref或reactive数据
};
return {
countObj,
person,
update
};
}
}).mount('#app');
</script>
在上面的代码中,我们分别定义了一个ref对象countObj和一个reactive对象person,同时定义了一个准备更新数据的update函数,并通过return返回数据。在模板中读取了ref对象和reactive对象的数据并进行动态显示,将update函数绑定给按钮的点击监听。运行后的页面效果如图所示。

此时使用watch函数来监听ref对象,默认进行的是浅监听,如果需要进行深度监听,则需要配置deep为true,代码如下。
js
//浅监听ref对象
watch(countObj, (newVal, oldVal)=> {
console.log('countObj浅监听', newVal, oldVal);
});
//深度监听ref对象
watch(
countObj,
(newVal, oldVal)=> {
console.log('countObj深度监听', newVal, oldVal);
},
{deep: true}
);
如果对ref对象数据进行浅更新,那么两个监听的回调都会执行。
js
const update = ()=> {
//对ref对象数据进行浅更新
countObj.value = {count: 2};
};

如果对ref对象数据进行深度更新,那么前面浅监听的回调不会再执行了。
js
const update = ()=> {
//对ref对象数据进行深度更新
countObj.value.count = 3;
};

使用watch函数监听reactive对象,默认进行的是深度监听。在按钮的点击回调中,无论是对person对象的浅更新,还是深度更新,监听的回调都会执行,代码如下。
js
//监听reactive对象,默认进行的是深度监听
watch(person, (newVal, oldVal)=> {
console.log('person change', newVal, oldVal);
});
const update = ()=> {
//浅更新
person.name += '--';
//深度更新
person.addr.city += '==';
};

如果我们要监听的是reactive对象代理的一个基础类型属性,比如现在想要监听name属性,如果直接给watch函数传入person.name,则程序会直接报错。这是因为它是一个基础类型的值,而不是一个响应式的值,代码如下。
js
//错误写法
watch(person.name, (newVal, oldVal)=> {
console.log('person.name change', newVal, oldVal);
});

Vue3针对这种情况,提供了函数式的写法,可以传入一个函数,函数内部返回要监听的这个值。
js
//正确写法
watch(()=> person.name, (newVal, oldVal)=> {
console.log('person.name change', newVal, oldVal);
});
const update = ()=> {
//浅更新
person.name += '--';
};

当用户点击"更新"按钮更新name属性值时,监听的回调就会自动执行。
如果我们要监听多个不同的数据,要怎么处理呢?其实watch函数可以接收一个包含多个要监听数据的数组。同时,watch函数接收的第1个参数就是由多个被监听数据的最新值组成的数组,第2个参数是旧值的数组。
js
//监听ref对象和reactive对象中的name属性
watch(
[countObj, ()=> person.name],
(newVals, oldVals)=> {
console.log('countObj或person.name变化了)', newVals, oldVals);
}
);

上面的代码监听的是ref对象countObj和reactive对象person中的name属性,其监听的回调接收的newVals就是最新countObj的value和最新person的name组成的数组,而oldVals就是旧countObj的value和旧person的name组成的数组。
如果在更新函数中对ref对象进行浅更新,监听的回调就会执行,代码如下。
js
const update = ()=> {
countObj.value = {count: 2};
};

如果在更新函数中通过reactive对象person更新name属性,监听的回调就会执行,代码如下。
js
const update = ()=> {
person.name += '--';
};

如果想让监听的回调在初始化时执行一次,就可以配置immediate为true。需要强调的是,还可以在监听的回调中执行异步操作,而这在计算属性的get函数中是不可以实现的,代码如下。
js
watch(countObj, (newVal, oldVal)=> {
console.log('立即执行的监听', newVal, oldVal);
//可以执行异步操作
setTimeout(()=> {
alert('2秒后的提示');
}, 2000);
}, {immediate: true}) ;
const update = ()=> {
countObj.value = {count: 2};
};

在初始化时监听的回调就会执行一次,且在2秒后显示警告提示,当然在点击按钮后,监听的回调还会再次执行。

11、生命周期钩子函数
组合式API的生命周期钩子函数和选项式API的生命周期钩子函数存在一定的差异。组合式API引入了setup函数来进行初始化操作,包括定义响应式数据、监听响应式数据,更新数据的函数等,而选项式API中的beforeCreate和created是在初始化过程中调用的两个生命周期钩子函数,所以Vue3中去掉了这两个生命周期钩子函数,用setup函数来代替它们。
除在初始阶段利用setup函数替换生命周期钩子函数beforeCreate和created以外,在组合式API中,挂载、更新、销毁阶段的生命周期钩子函数都以函数监听的方式实现,包括onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted。值得一提的是,要想使用这些生命周期钩子函数,必须先从Vue中引入,然后在setup函数中调用,生命周期钩子函数的调用结果主要通过回调函数的形式实现。
组合式API的生命周期钩子函数共有4个阶段,如图所示。下面结合代码进行具体说明。

html
<div id="app">
<p ref="mRef">{{message}}</p>
<button @click="message='互联网技术培训'">更新内容</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {
reactive,
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} = Vue;
const vm = Vue.createApp({
setup(props) {
//定义一个ref元素对象
const mRef = ref(null);
//定义响应式数据message
const message = ref('欢迎来到尚硅谷');
//onBeforeMount没有完成挂载,有message却没有ref元素对象
onBeforeMount(()=> {
console.log('onBeforeMount', message, mRef.value);
});
//onMounted已经完成挂载,message与ref元素对象都存在
onMounted(()=> {
console.log('onMounted', message, mRef.value);
});
//更新之前的页面
onBeforeUpdate(()=> {
console.log('onBeforeUpdate');
debugger;
});
//更新之后的页面
onUpdated(()=> {
console.log('onUpdated');
});
//组件实例完全销毁前,仍旧有元素对象(当前就是p标签对象)
onBeforeUnmount(()=> {
console.log('onBeforeUnmount', mRef.value);
debugger;
});
//组件实例完全销毁,不存在元素对象
onUnmounted(()=> {
console.log('onUnmounted', mRef.value);
});
return {
mRef,
message
}
}
}).mount('#app');
</script>

结合上面的代码分析组合式API的生命周期钩子函数。
setup:在setup初始阶段,利用ref函数定义一个响应式数据message,并设置其初始值为"欢迎来到尚硅谷"。onBeforeMount:给模板中的p元素添加ref属性mRef,并在脚本中设置mRef为ref类型,在onBeforeMount生命周期钩子函数中打印message和mRef.value,可以看到message有数据,而mRef.value为null。这是因为DOM元素现在仍存在于内存中,并没有完全完成挂载。onMounted:这一阶段网页的el元素对象成功地将虚拟DOM内容渲染到了真实DOM对象上,此时打印mRef.value,其值是一个p元素标签的内容,说明挂载已经完成。onBeforeUpdate:我们可以点击页面中的按钮来修改响应式数据message,在onBeforeUpdate生命周期钩子函数中设置一个debugger断点查看效果,最终会发现页面中仍旧显示的是数据修改之前的DOM元素内容。onUpdated:与onBeforeUpdate类似,在onUpdated生命周期钩子函数中设置debugger断点,查看到的是数据发生改变以后的页面。onBeforeUnmount:当这个生命周期钩子函数被调用时,组件实例依然拥有全部的功能。同样也在该生命周期钩子函数中进行debugger断点调试。在利用定时器进行unmount销毁操作后,则可以查看到即将进入onBeforeUnmount生命周期阶段。此时页面中仍旧保留DOM显示的状态,控制台中的mRef.value仍旧有元素对象。onUnmounted:该生命周期钩子函数在一个组件实例被销毁后调用,对应组件实例的DOM对象也将不复存在。继续进行debugger断点调试,程序就会进入onUnmounted生命周期阶段。此时组件原本指向的DOM对象已经清空,页面中也不再显示任何的DOM元素内容。