1. Vue3简介
- 2020年9月18日,
Vue.js
发布版3.0
版本,代号:One Piece
(n - 经历了:4800+次提交、40+个RFC、600+次PR、300+贡献者
- 官方发版地址:Release v3.0.0 One Piece · vuejs/core
1.1. 【性能的提升】
打包大小减少
41%
。初次渲染快
55%
, 更新渲染快133%
。内存减少
54%
。
1.2.【 源码的升级】
使用
Proxy
代替defineProperty
实现响应式。重写虚拟
DOM
的实现和Tree-Shaking
。
1.3. 【拥抱TypeScript】
Vue3
可以更好的支持TypeScript
。
1.4. 【新的特性】
Composition API
(组合API
):setup
ref
与reactive
computed
与watch
......
新的内置组件:
Fragment
Teleport
Suspense
......
其他改变:
新的生命周期钩子
data
选项应始终被声明为一个函数移除
keyCode
支持作为v-on
的修饰符......
2. 创建Vue3工程
2.1. 【基于 vue-cli 创建】(不推荐)
点击查看官方文档
备注:目前
vue-cli
已处于维护模式,官方推荐基于Vite
创建项目。
### 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
### 安装或者升级你的@vue/cli
npm install -g @vue/cli
### 执行创建命令
vue create vue_test
### 随后选择3.x
### Choose a version of Vue.js that you want to start the project with (Use arrow keys)
### > 3.x
### 2.x
### 启动
cd vue_test
npm run serve
2.2. 【基于 vite 创建】(推荐)
vite
是新一代前端构建工具,官网地址:https://vitejs.cn,vite
的优势如下:
- 轻量快速的热重载(
HMR
),能实现极速的服务启动。 - 对
TypeScript
、JSX
、CSS
等支持开箱即用。 - 真正的按需编译,不再等待整个应用编译完成。
webpack
构建 与vite
构建对比图如下:
- 具体操作如下(点击查看官方文档)
### 1.创建命令
npm create vue@latest
### 2.具体配置
### 配置项目名称
√ Project name: vue3_test
### 是否添加TypeScript支持
√ Add TypeScript? Yes
### 是否添加JSX支持
√ Add JSX Support? No
### 是否添加路由环境
√ Add Vue Router for Single Page Application development? No
### 是否添加pinia环境
√ Add Pinia for state management? No
### 是否添加单元测试
√ Add Vitest for Unit Testing? No
### 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
### 是否添加ESLint语法检查
√ Add ESLint for code quality? Yes
### 是否添加Prettiert代码格式化
√ Add Prettier for code formatting? No
自己动手编写一个App组件
<template>
<div class="app">
<h1>你好啊!</h1>
</div>
</template>
<script lang="ts">
export default {
name:'App' //组件名
}
</script>
<style>
.app {
background-color: #ddd;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
</style>
安装官方推荐的vscode
插件:
开发vue3项目时,需要禁用
Vetur
插件,Vetur
插件适用于vue2项目,在vue3项目中会出现各种奇怪的问题


总结:
Vite
项目中,index.html
是项目的入口文件,在项目最外层。- 加载
index.html
后,Vite
解析<script type="module" src="xxx">
指向的JavaScript
。 Vue3
**中是通过 **createApp
函数创建一个应用实例。
2.3. 【一个简单的效果】
Vue3
向下兼容Vue2
语法,且Vue3
中的模板中可以没有根标签
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name:'App',
data() {
return {
name:'张三',
age:18,
tel:'13888888888'
}
},
methods:{
changeName(){
this.name = 'zhang-san'
},
changeAge(){
this.age += 1
},
showTel(){
alert(this.tel)
}
},
}
</script>
3. Composition API
3.1. 【OptionsAPI 与 CompositionAPI】
Vue2
的API
设计是Options
(配置)风格的。Vue3
的API
设计是Composition
(组合)风格的。
Options API 的弊端
Options
类型的 API
,数据、方法、计算属性等,是分散在:data
、methods
、computed
中的,若想新增或者修改一个需求,就需要分别修改:data
、methods
、computed
,不便于维护和复用。
Composition API 的优势
可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
说明:以上四张动图原创作者:大帅老猿
3.2. 【拉开序幕的 setup】
setup 概述
setup
是Vue3
中一个新的配置项,值是一个函数,它是 Composition API
“表演的舞台”,组件中所用到的:数据、方法、计算属性、监视......等等,均配置在setup
中。
特点如下:
setup
函数返回的对象中的内容,可直接在模板中使用。setup
中访问this
是undefined
。setup
函数会在beforeCreate
之前调用,它是“领先”所有钩子执行的。
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name:'Person',
setup(){
// 数据,原来写在data中(注意:此时的name、age、tel数据都不是响应式数据)
let name = '张三'
let age = 18
let tel = '13888888888'
// 方法,原来写在methods中
function changeName(){
name = 'zhang-san' //注意:此时这么修改name页面是不变化的
console.log(name)
}
function changeAge(){
age += 1 //注意:此时这么修改age页面是不变化的
console.log(age)
}
function showTel(){
alert(tel)
}
// 返回一个对象,对象中的内容,模板中可以直接使用
return {name,age,tel,changeName,changeAge,showTel}
}
}
</script>
setup 的返回值
- 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用**(重点关注)。**
- 若返回一个函数:则可以自定义渲染内容,代码如下:
setup(){
return ()=> '你好啊!'
}
setup 与 Options API 的关系
Vue2
的配置(data
、methos
......)中可以访问到setup
中的属性、方法。- 但在
setup
中不能访问到Vue2
的配置(data
、methos
......)。 - 如果与
Vue2
冲突,则setup
优先。
setup 语法糖
setup
函数有一个语法糖,这个语法糖,可以让我们把setup
独立出去,代码如下:
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changName">修改名字</button>
<button @click="changAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name:'Person',
}
</script>
<!-- 下面的写法是setup语法糖 -->
<script setup lang="ts">
console.log(this) //undefined
// 数据(注意:此时的name、age、tel都不是响应式数据)
let name = '张三'
let age = 18
let tel = '13888888888'
// 方法
function changName(){
name = '李四'//注意:此时这么修改name页面是不变化的
}
function changAge(){
console.log(age)
age += 1 //注意:此时这么修改age页面是不变化的
}
function showTel(){
alert(tel)
}
</script>
扩展:上述代码,还需要编写一个不写setup
的script
标签,去指定组件名字,比较麻烦,我们可以借助vite
中的插件简化
- 第一步:
npm i vite-plugin-vue-setup-extend -D
- 第二步:
vite.config.ts
import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [ VueSetupExtend() ]
})
- 第三步:
<script setup lang="ts" name="Person">
4. ref和reactive
4.1. 【ref 创建:基本类型的响应式数据】
- **作用:**定义响应式变量。
- 语法:
let xxx = ref(初始值)
。 - **返回值:**一个
RefImpl
的实例对象,简称ref对象
或ref
,ref
对象的value
属性是响应式的。 - 注意点:
JS
中操作数据需要:xxx.value
,但模板中不需要.value
,直接使用即可。- 对于
let name = ref('张三')
来说,name
不是响应式的,name.value
是响应式的。
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script setup lang="ts" name="Person">
import {ref} from 'vue'
// name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。
let name = ref('张三')
let age = ref(18)
// tel就是一个普通的字符串,不是响应式的
let tel = '13888888888'
function changeName(){
// JS中操作ref对象时候需要.value
name.value = '李四'
console.log(name.value)
// 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。
// name = ref('zhang-san')
}
function changeAge(){
// JS中操作ref对象时候需要.value
age.value += 1
console.log(age.value)
}
function showTel(){
alert(tel)
}
</script>
4.2. 【reactive 创建:对象类型的响应式数据】
- 作用:定义一个响应式对象(基本类型不要用它,要用
ref
,否则报错) - 语法:
let 响应式对象= reactive(源对象)
。 - **返回值:**一个
Proxy
的实例对象,简称:响应式对象。 - 注意点:
reactive
定义的响应式数据是“深层次”的。
<template>
<div class="person">
<h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
<h2>游戏列表:</h2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<h2>测试:{{obj.a.b.c.d}}</h2>
<button @click="changeCarPrice">修改汽车价格</button>
<button @click="changeFirstGame">修改第一游戏</button>
<button @click="test">测试</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { reactive } from 'vue'
// 数据
let car = reactive({ brand: '奔驰', price: 100 })
let games = reactive([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = reactive({
a:{
b:{
c:{
d:666
}
}
}
})
function changeCarPrice() {
car.price += 10
}
function changeFirstGame() {
games[0].name = '流星蝴蝶剑'
}
function test(){
obj.a.b.c.d = 999
}
</script>
4.3. 【ref 创建:对象类型的响应式数据】
- 其实
ref
接收的数据可以是:基本类型、对象类型。 - 若
ref
接收的是对象类型,内部其实也是调用了reactive
函数。
<template>
<div class="person">
<h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
<h2>游戏列表:</h2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<h2>测试:{{obj.a.b.c.d}}</h2>
<button @click="changeCarPrice">修改汽车价格</button>
<button @click="changeFirstGame">修改第一游戏</button>
<button @click="test">测试</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref } from 'vue'
// 数据
let car = ref({ brand: '奔驰', price: 100 })
let games = ref([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = ref({
a:{
b:{
c:{
d:666
}
}
}
})
console.log(car)
function changeCarPrice() {
car.value.price += 10
}
function changeFirstGame() {
games.value[0].name = '流星蝴蝶剑'
}
function test(){
obj.value.a.b.c.d = 999
}
</script>
4.4. 【ref 对比 reactive】
宏观角度看:
ref
用来定义:基本类型数据、对象类型数据;
reactive
用来定义:对象类型数据。
- 区别:
ref
创建的变量必须使用.value
(可以使用volar
插件自动添加.value
)。
reactive
重新分配一个新对象,会失去响应式(可以使用Object.assign
去整体替换)。
- 使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref
。- 若需要一个响应式对象,层级不深,
ref
、reactive
都可以。- 若需要一个响应式对象,且层级较深,推荐使用
reactive
。
4.5. 【toRefs 与 toRef】
- 作用:将一个响应式对象中的每一个属性,转换为
ref
对象。 - 备注:
toRefs
与toRef
功能一致,但toRefs
可以批量转换。 - 语法如下:
<template>
<div class="person">
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>性别:{{person.gender}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,reactive,toRefs,toRef} from 'vue'
// 数据
let person = reactive({name:'张三', age:18, gender:'男'})
// 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
let {name,gender} = toRefs(person)
// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
let age = toRef(person,'age')
// 方法
function changeName(){
name.value += '~'
}
function changeAge(){
age.value += 1
}
function changeGender(){
gender.value = '女'
}
</script>
4.6.【customRef】自定义ref实现防抖
作用:创建一个自定义的ref
,并对其依赖项跟踪和更新触发进行逻辑控制。
实现防抖效果(useSumRef.ts
):
import { customRef } from "vue";
export default function(initValue:string,delay:number){
const msg = customRef((track,trigger)=>{
let timer:number
return {
get(){
track() // 告诉Vue数据msg很重要,要对msg持续关注,一旦变化就更新
return initValue
},
set(value){
clearTimeout(timer)
timer = setTimeout(() => {
initValue = value
trigger() //通知Vue数据msg变化了
}, delay);
}
}
})
return msg
}
组件中使用:
<template>
<input v-model="name">
{{name}}
</template>
<script lang="ts" setup name="App">
import useSumRef from "@/utils/useSumRef"
const name = useSumRef('张三',1000)
</script>

5. Vue3核心语法
5.1. 【computed】
作用:根据已有数据计算出新数据(和Vue2
中的computed
作用一致)。
只有计算属性值的地址发生改变了才会调用set方法,修改对象的某个属性不会调用set方法(类似于watch的浅度监听)

<template>
<div class="person">
姓:<input type="text" v-model="firstName"> <br>
名:<input type="text" v-model="lastName"> <br>
全名:<span>{{fullName}}</span> <br>
<button @click="changeFullName">全名改为:li-si</button>
</div>
</template>
<script setup lang="ts" name="App">
import {ref,computed} from 'vue'
let firstName = ref('zhang')
let lastName = ref('san')
// 计算属性——只读取,不修改
/* let fullName = computed(()=>{
return firstName.value + '-' + lastName.value
}) */
// 计算属性——既读取又修改
let fullName = computed({
// 读取
get(){
return firstName.value + '-' + lastName.value
},
// 修改
set(val){
console.log('有人修改了fullName',val)
firstName.value = val.split('-')[0]
lastName.value = val.split('-')[1]
}
})
function changeFullName(){
fullName.value = 'li-si'
}
</script>
如下代码计算属性只有地址发生了改变才会调用set方法,只修改某个属性时不会调用set方法,因此建议计算属性值尽量为基本类型
<template>
<div class="person">
名字:<input type="text" v-model="person.name"> <br>
年龄:<input type="number" v-model="person.age"> <br>
信息:<span>{{personInfo}}</span> <br>
<button @click="changeName">名字改为:jerry</button>
<button @click="changeInfo">修改人员信息</button>
</div>
</template>
<script setup lang="ts" name="App">
import { reactive,computed } from 'vue'
const person = reactive({name:'tom',age:18})
// 计算属性——既读取又修改
let personInfo = computed({
// 读取
get(){
return {name:person.name+'a',age:person.age+1}
},
// 修改
set(val){
console.log('有人修改了人员信息',val)
}
})
function changeName(){
// 计算属性值的地址没有改变,因此不会调用set方法
personInfo.value.name = 'jerry'
}
function changeInfo() {
// 计算属性值的地址发生改变了因此会调用set方法
personInfo.value = {name:'jerry',age:16}
}
</script>
5.2.【watch】
- 作用:监视数据的变化(和
Vue2
中的watch
作用一致) - 特点:
Vue3
中的watch
只能监视以下四种数据:
ref
定义的数据。reactive
定义的数据。- 函数返回一个值(
getter
函数)。- 一个包含上述内容的数组。
我们在Vue3
中使用watch
的时候,通常会遇到以下几种情况:
* 情况一
监视ref
定义的【基本类型】数据:直接写数据名即可,监视的是其value
值的改变。
监视
ref
定义的数据(不管是基本类型还是对象类型),使用watch(sum,(newValue,oldValue)=>{}
和watch(()=>sum.value,(newValue,oldValue)=>{}
效果是一样的
<template>
<div class="person">
<h1>情况一:监视【ref】定义的【基本类型】数据</h1>
<h2>当前求和为:{{sum}}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,watch} from 'vue'
// 数据
let sum = ref(0)
// 方法
function changeSum(){
sum.value += 1
}
// 监视,情况一:监视【ref】定义的【基本类型】数据
const stopWatch = watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
if(newValue >= 10){
stopWatch()
}
})
</script>
* 情况二
监视ref
定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
注意:
若修改的是
ref
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。若修改整个
ref
定义的对象,newValue
是新值,oldValue
是旧值,因为不是同一个对象了。监视
ref
定义的数据(不管是基本类型还是对象类型),使用watch(sum,(newValue,oldValue)=>{}
和watch(()=>sum.value,(newValue,oldValue)=>{}
效果是一样的
<template>
<div class="person">
<h1>情况二:监视【ref】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,watch} from 'vue'
// 数据
let person = ref({
name:'张三',
age:18
})
// 方法
function changeName(){
person.value.name += '~'
}
function changeAge(){
person.value.age += 1
}
function changePerson(){
person.value = {name:'李四',age:90}
}
/*
监视,情况一:监视【ref】定义的【对象类型】数据,监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视
watch的第一个参数是:被监视的数据
watch的第二个参数是:监视的回调
watch的第三个参数是:配置对象(deep、immediate等等.....)
*/
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{deep:true})
</script>
* 情况三
监视reactive
定义的【对象类型】数据,且默认开启了深度监视。
<template>
<div class="person">
<h1>情况三:监视【reactive】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
<hr>
<h2>测试:{{obj.a.b.c}}</h2>
<button @click="test">修改obj.a.b.c</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18
})
let obj = reactive({
a:{
b:{
c:666
}
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changePerson(){
Object.assign(person,{name:'李四',age:80})
}
function test(){
obj.a.b.c = 888
}
// 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
})
watch(obj,(newValue,oldValue)=>{
console.log('Obj变化了',newValue,oldValue)
})
</script>
* 情况四
监视ref
或reactive
定义的【对象类型】数据中的某个属性,注意点如下:
- 若该属性值不是【对象类型】,需要写成函数形式。
- 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。
结论:监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
<template>
<div class="person">
<h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeC1">修改第一台车</button>
<button @click="changeC2">修改第二台车</button>
<button @click="changeCar">修改整个车</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changeC1(){
person.car.c1 = '奥迪'
}
function changeC2(){
person.car.c2 = '大众'
}
function changeCar(){
person.car = {c1:'雅迪',c2:'爱玛'}
}
// 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
/* watch(()=> person.name,(newValue,oldValue)=>{
console.log('person.name变化了',newValue,oldValue)
}) */
// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
watch(()=>person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
* 情况五
监视上述的多个数据
<template>
<div class="person">
<h1>情况五:监视上述的多个数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeC1">修改第一台车</button>
<button @click="changeC2">修改第二台车</button>
<button @click="changeCar">修改整个车</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changeC1(){
person.car.c1 = '奥迪'
}
function changeC2(){
person.car.c2 = '大众'
}
function changeCar(){
person.car = {c1:'雅迪',c2:'爱玛'}
}
// 监视,情况五:监视上述的多个数据
watch([()=>person.name,person.car],(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
5.3. 【watchEffect】
官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
watch
对比watchEffect
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
:要明确指出监视的数据watchEffect
:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
示例代码:
<template> <div class="person"> <h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1> <h2 id="demo">水温:{{temp}}</h2> <h2>水位:{{height}}</h2> <button @click="changePrice">水温+1</button> <button @click="changeSum">水位+10</button> </div> </template> <script lang="ts" setup name="Person"> import {ref,watch,watchEffect} from 'vue' // 数据 let temp = ref(0) let height = ref(0) // 方法 function changePrice(){ temp.value += 10 } function changeSum(){ height.value += 1 } // 用watch实现,需要明确的指出要监视:temp、height watch([temp,height],(value)=>{ // 从value中获取最新的temp值、height值 const [newTemp,newHeight] = value // 室温达到50℃,或水位达到20cm,立刻联系服务器 if(newTemp >= 50 || newHeight >= 20){ console.log('联系服务器') } }) // 用watchEffect实现,不用指明监视哪些数据 const stopWtach = watchEffect(()=>{ // 室温达到50℃,或水位达到20cm,立刻联系服务器 if(temp.value >= 50 || height.value >= 20){ console.log(document.getElementById('demo')?.innerText) console.log('联系服务器') } // 水温达到100,或水位达到50,取消监视 if(temp.value === 100 || height.value === 50){ console.log('清理了') // 调用watchEffect返回的函数以停止监视 stopWtach() } }) </script>
5.4. 【标签的 ref 属性】
* 方式一:定义同名的ref
作用:用于注册模板引用。
用在普通
DOM
标签上,获取的是DOM
节点。用在组件标签上,获取的是组件实例对象。
用在普通DOM
标签上:
<template>
<div class="person">
<h1 ref="title">Vue</h1>
<button @click="showLog">点我打印内容</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref} from 'vue'
// ref="title"中的title与该变量同名,vue会将组件的ref自动设置到该变量上
let title = ref()
function showLog(){
// 通过ref获取元素
console.log(title.value)
}
</script>
用在组件标签上:
<!-- 父组件App.vue -->
<template>
<Person ref="ren"/>
<button @click="test">测试</button>
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {ref} from 'vue'
let ren = ref()
function test(){
console.log(ren.value.name)
console.log(ren.value.age)
}
</script>
<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
import {ref,defineExpose} from 'vue'
// 数据
let name = ref('张三')
let age = ref(18)
/****************************/
/****************************/
// 使用defineExpose将组件中的数据交给外部
defineExpose({name,age})
</script>
* 方式二:通过函数动态设置ref
通过
:ref
传入一个函数,可以动态的设置ref
<template>
<div class="person">
<template v-for="(item,index) in data" :key="item.label">
<div :index="index" :value="item.value" :ref="(divRef)=>setRef(divRef,index)">{{ item.label }}</div>
</template>
<button @click="showLog">点我打印内容</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive } from 'vue'
const data = [
{label: 'tom', value: 18},
{label: 'jerry', value: 19},
{label: 'spike', value: 20}
]
let refs = reactive([])
function setRef(divRef:any,index :Number){
refs[index] = divRef
}
function showLog() {
// 通过ref获取元素
console.log(refs)
}
</script>

5.5. 【defineExpose 定义暴露】
组件可以通过defineExpose将该组件的数据
和方法
暴露出去供其他组件使用
<template>
<div v-if="visible">
子组件打开,父组件传递的数据为: {{ personInfo }}
</div>
</template>
<script lang="ts" setup name="Person">
import {ref, defineExpose, reactive} from 'vue'
const visible = ref(false)
const personInfo = reactive({})
const clerarObjectData = (obj) => {
Object.keys(obj).forEach((key)=>{
// personInfo[key] = void 0
delete obj[key]
})
}
// 数据
const name = ref('张三')
const age = ref(18)
const personRef = ref({name:'张三',age:18})
const personReactive = reactive({name:'张三',age:18})
// 方法
const onOpen = (record) => {
visible.value = true
// 清空personInfo对象数据
clerarObjectData(personInfo)
// 将record数据赋值给personInfo
Object.assign(personInfo,record)
}
const onClose = () => {
visible.value = false
}
/****************************/
/****************************/
// 使用defineExpose将组件中的数据交给外部
defineExpose({name,age,personRef,personReactive,onOpen,onClose})
</script>
父组件获取Person组件暴露出来的数据和方法
可以看到暴露的不管是ref还是reactive都是可以直接使用和修改的,ref对象显示的虽然是RefImpl,但是获取数据并不需要
.value
<template>
<Person ref="ren"/>
<hr/>
<button @click="test">测试</button>
</template>
<script lang="ts" setup name="App">
import {ref} from 'vue'
let ren = ref()
function test() {
console.log(ren.value)
console.log(ren.value.name)
console.log(ren.value.age)
console.log(ren.value.personRef)
console.log(ren.value.personReactive)
console.log(ren.value.onOpen)
console.log(ren.value.onClose)
ren.value.name = '王五'
console.log(ren.value.name)
ren.value.onOpen({name: '李四', age: 16})
}
</script>

5.6. 【props】
// 定义一个接口,限制每个Person对象的格式
export interface PersonInter {
id:string,
name:string,
age:number
}
// 定义一个自定义类型Persons
export type Persons = Array<PersonInter>
App.vue
中代码:
<template>
<Person :list="persons"/>
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {reactive} from 'vue'
import {type Persons} from './types'
let persons = reactive<Persons>([
{id:'e98219e12',name:'张三',age:18},
{id:'e98219e13',name:'李四',age:19},
{id:'e98219e14',name:'王五',age:20}
])
</script>
Person.vue
中代码:
在template里可以使用props.list和list,如果也script定义了list会优先使用script里的list,此时可以使用props.list获取props里的list;在script里只能使用props.list。如下代码展示了props里有list字段同时script里也有list变量的情况
<template> <div class="person"> {{list}} <br /> {{props.list}} </div> </template> <script lang="ts" setup name="Person"> import {defineProps} from 'vue' import {type Persons} from '@/types' const list = [{id:'1',name:'2',age:19}] const props = defineProps(['list']) </script>
image-20240219213329582
<template>
<div class="person">
<ul>
<li v-for="item in list" :key="item.id">
{{item.name}}--{{item.age}}
</li>
</ul>
</div>
</template>
<script lang="ts" setup name="Person">
import {defineProps} from 'vue'
import {type PersonInter} from '@/types'
// 第一种写法:仅接收
// const props = defineProps(['list'])
// 第二种写法:接收+限制类型
// defineProps<{list:Persons}>()
// 第三种写法:接收+限制类型+指定默认值+限制必要性
let props = withDefaults(defineProps<{list?:Persons}>(),{
list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
})
console.log(props)
</script>
需要注意的是,props传递的不管是ref还是reactive,接收的统统都是reactive对象
App.vue
中代码:
<template>
<Person :list="persons" :personRef="refData" :personReactive="reactiveData"/>
</template>
<script lang="ts" setup name="App">
import Person from '@/components/Person.vue'
import {ref,reactive} from 'vue'
import {type Persons} from '@/types'
let refData = ref({id:'e98219e12',name:'张三',age:18})
let reactiveData = reactive({id:'e98219e13',name:'李四',age:19})
let persons = reactive<Persons>([
{id:'e98219e12',name:'张三',age:18},
{id:'e98219e13',name:'李四',age:19},
{id:'e98219e14',name:'王五',age:20}
])
</script>
Person.vue
中代码:
<script lang="ts" setup name="Person">
import {defineProps} from 'vue'
const props = defineProps(['list','personRef','personReactive'])
console.log(props)
</script>

5.7. 【生命周期】
概念:
Vue
组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue
会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子规律:
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
Vue2
的生命周期创建阶段:
beforeCreate
、created
挂载阶段:
beforeMount
、mounted
更新阶段:
beforeUpdate
、updated
销毁阶段:
beforeDestroy
、destroyed
Vue3
的生命周期创建阶段:
setup
挂载阶段:
onBeforeMount
、onMounted
更新阶段:
onBeforeUpdate
、onUpdated
卸载阶段:
onBeforeUnmount
、onUnmounted
常用的钩子:
onMounted
(挂载完毕)、onUpdated
(更新完毕)、onBeforeUnmount
(卸载之前)示例代码:
<template> <div class="person"> <h2>当前求和为:{{ sum }}</h2> <button @click="changeSum">点我sum+1</button> </div> </template> <!-- vue3写法 --> <script lang="ts" setup name="Person"> import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' // 数据 let sum = ref(0) // 方法 function changeSum() { sum.value += 1 } console.log('setup') // 生命周期钩子 onBeforeMount(()=>{ console.log('挂载之前') }) onMounted(()=>{ console.log('挂载完毕') }) onBeforeUpdate(()=>{ console.log('更新之前') }) onUpdated(()=>{ console.log('更新完毕') }) onBeforeUnmount(()=>{ console.log('卸载之前') }) onUnmounted(()=>{ console.log('卸载完毕') }) </script>
5.8. 【自定义hook】
什么是
hook
?—— 本质是一个函数,把setup
函数中使用的Composition API
进行了封装,类似于vue2.x
中的mixin
。自定义
hook
的优势:复用代码, 让setup
中的逻辑更清楚易懂。
示例代码:
useSum.ts
中内容如下:import {ref,onMounted} from 'vue' export default function(){ let sum = ref(0) const increment = ()=>{ sum.value += 1 } const decrement = ()=>{ sum.value -= 1 } onMounted(()=>{ increment() }) //向外部暴露数据 return {sum,increment,decrement} }
useDog.ts
中内容如下:import {reactive,onMounted} from 'vue' import axios,{AxiosError} from 'axios' export default function(){ let dogList = reactive<string[]>([]) // 方法 async function getDog(){ try { // 发请求 let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/images/random') // 维护数据 dogList.push(data.message) } catch (error) { // 处理错误 const err = <AxiosError>error console.log(err.message) } } // 挂载钩子 onMounted(()=>{ getDog() }) //向外部暴露数据 return {dogList,getDog} }
组件中具体使用:
<template> <h2>当前求和为:{{sum}}</h2> <button @click="increment">点我+1</button> <button @click="decrement">点我-1</button> <hr> <img v-for="(u,index) in dogList.urlList" :key="index" :src="(u as string)"> <span v-show="dogList.isLoading">加载中......</span><br> <button @click="getDog">再来一只狗</button> </template> <script lang="ts"> import {defineComponent} from 'vue' export default defineComponent({ name:'App', }) </script> <script setup lang="ts"> import useSum from './hooks/useSum' import useDog from './hooks/useDog' let {sum,increment,decrement} = useSum() let {dogList,getDog} = useDog() </script>
5.9. 【nextTick更新DOM后再执行】
当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
nextTick()
可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。
方式一:传递回调函数
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
function increment() {
count.value++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
nextTick(()=>{
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
})
}
</script>
方式二:await 返回的 Promise
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
</script>
6. 路由
6.1. 【对路由的理解】

6.2. 【基本切换效果】
Vue3
中要使用vue-router
的最新版本,目前是4
版本。路由配置文件代码如下:
import { createRouter, createWebHistory } from 'vue-router' import Home from '@/pages/Home.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/home', component: Home }, { path: '/news', // 路由懒加载 component: () => import('@/pages/News.vue') }, { path: '/about', component: () => import('@/pages/About.vue') } ] }) export default router
main.ts
代码如下:import router from './router/index' app.use(router) app.mount('#app')
App.vue
代码如下<template> <div class="app"> <h2 class="title">Vue路由测试</h2> <!-- 导航区 --> <div class="navigate"> <router-link to="/home" active-class="active">首页</router-link> <router-link to="/news" active-class="active">新闻</router-link> <router-link to="/about" active-class="active">关于</router-link> </div> <!-- 展示区 --> <div class="main-content"> <router-view></router-view> </div> </div> </template> <script lang="ts" setup name="App"> import {RouterLink,RouterView} from 'vue-router' </script>
6.3. 【两个注意点】
路由组件通常存放在
pages
或views
文件夹,一般组件通常存放在components
文件夹。通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载。
6.4.【路由器工作模式】
history
模式优点:
URL
更加美观,不带有#
,更接近传统的网站URL
。类似于这样: http://localhost:5173/about缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有
404
错误。const router = createRouter({ history:createWebHistory(), //history模式 /******/ })
hash
模式优点:兼容性更好,因为不需要服务器端处理路径。
缺点:
URL
带有#
不太美观,且在SEO
优化方面相对较差。类似于这样:http://localhost:5173/#/aboutconst router = createRouter({ history:createWebHashHistory(), //hash模式 /******/ })
6.5. 【to的两种写法】
<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>
<!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>
6.6. 【命名路由】
作用:可以简化路由跳转及传参(后面就讲)。
给路由规则命名:
routes: [
{
name: 'zhuye',
path: '/home',
component: Home
},
{
name: 'xinwen',
path: '/news',
component: () => import('@/pages/News.vue')
},
{
name: 'guanyu',
path: '/about',
component: () => import('@/pages/About.vue')
}
]
跳转路由:
<router-link to="/about">关于</router-link>
<router-link :to="{name:'guanyu'}">关于</router-link>
6.7. 【嵌套路由】
编写
News
的子路由:Detail.vue
配置路由规则,使用
children
配置项:
const router = createRouter({
history:createWebHistory(),
routes:[
{
name:'zhuye',
path:'/home',
component:Home
},
{
name:'xinwen',
path:'/news',
component:() => import('@/pages/News.vue'),
children:[
{
name:'xiang',
path:'detail',
component:() => import('@/pages/Detail.vue')
}
]
},
{
name:'guanyu',
path:'/about',
component:() => import('@/pages/About.vue')
}
]
})
export default router
- 跳转路由(记得要加完整路径):
<router-link to="/news/detail">详情</router-link>
<router-link :to="{path:'/news/detail'}">详情</router-link>
<router-link :to="{name:'xiang'}">详情</router-link>
- 记得去
News
组件中预留一个<router-view>
<template>
<div class="news">
<nav class="news-list">
<router-link v-for="news in newsList" :key="news.id" :to="{path:'/news/detail'}">
{{news.title}}
</router-link>
</nav>
<div class="news-detail">
<router-view/>
</div>
</div>
</template>
<script setup lang="ts">
import {reactive} from "vue"
const newsList = reactive([
{id:1,title:'新闻1',content:'内容1'},
{id:2,title:'新闻2',content:'内容2'},
{id:3,title:'新闻3',content:'内容3'},
])
</script>
6.8. 【路由传参】
query参数
query参数类似 http://localhost:5173/news/detail?id=1&title=新闻1&content=内容1
这样,使用&
分隔键值对
- 传递参数
<!-- 跳转并携带query参数(to的字符串写法) -->
<div v-for="news in newsList" :key="news.id">
<router-link :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">
{{news.title}}
</router-link>
</div>
<br />
<!-- 跳转并携带query参数(to的对象写法) -->
<div v-for="news in newsList" :key="news.id">
<router-link
:to="{
//name:'xiang', //用name也可以跳转
path:'/news/detail',
query:{
id:news.id,
title:news.title,
content:news.content
}
}"
>
{{ news.title }}
</router-link>
</div>
- 接收参数:
Detail
组件可以使用useRoute
获取query参数
<template>
{{route.query}}
</template>
<script setup>
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印query参数
console.log(route.query)
</script>
params参数
params参数类似 http://localhost:5173/news/detail/1/新闻1/内容1
这样,将数据写到路径上
- 修改path
需要将路由里的path从
detail
改为detail/:id/:title/:content
src/router/index.ts
const router = createRouter({
history:createWebHistory(),
routes:[
..
{
name:'xinwen',
path:'/news',
component:() => import('@/pages/News.vue'),
children:[
{
name:'xiang',
path:'detail/:id/:title/:content', // 将 `detail`改为`detail/:id/:title/:content`
component:() => import('@/pages/Detail.vue')
}
]
},
..
]
})
export default router
- 传递参数
<!-- 跳转并携带params参数(to的字符串写法) -->
<div v-for="news in newsList" :key="news.id">
<router-link :to="`/news/detail/${news.id}/${news.title}/${news.content}`">
{{news.title}}
</router-link>
</div>
<br />
<!-- 跳转并携带params参数(to的对象写法) -->
<div v-for="news in newsList" :key="news.id">
<router-link
:to="{
name:'xiang', //用name跳转 (使用对象写法 to中传递params参数 不能使用path只能使用name)
params:{
id:news.id,
title:news.title,
content:news.content
}
}"
>
{{news.title}}
</router-link>
</div>
- 接收参数:
<template>
{{route.params}
</template>
<script setup>
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印params参数
console.log(route.params)
</script>
备注1:传递
params
参数时,若使用to
的对象写法,必须使用name
配置项,不能用path
。备注2:传递
params
参数时,需要提前在规则中占位。
存在的问题:
上述写法必须给所有的数据都指定值才能正确匹配到路由,比如http://localhost:5173/news/detail/1/新闻1/内容1
这样才能正确访问,此时我们访问http://localhost:5173/news/detail
会发现没有显示详情的那个组件

而我们有时可以允许不指定某些数据,比如不指定content,也就是通过http://localhost:5173/news/detail/1/新闻1
也能访问到,我们可以将src/router/index.ts
文件里指定路由的path从detail/:id/:title/:content
改为detail/:id/:title/:content?
,这样我们就能通过 http://localhost:5173/news/detail/1/新闻1
访问到该组件了。如果三个都可以为空,则将path写成detail/:id?/:title?/:content?
即可,此时也能正确的匹配到路由

6.9. 【路由的props配置】
作用:让路由组件更方便的收到参数(可以将路由参数作为props
传给组件)
{
name:'xiang',
path:'detail/:id?/:title?/:content?',
component:() => import('@/pages/Detail.vue'),
// props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件
// props:{a:1,b:2,c:3},
// props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件
// props:true
// props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件
props(route){
return route.params
}
}
Detail组件接收传过来的props参数
<template>
{{props}}
</template>
<script setup>
import {defineProps} from 'vue'
const props = defineProps(['id','title','content'])
</script>
6.10. 【meta 路由额外信息】
有时我们希望给某个路由增加一些额外的信息,比如这个路由显不显示在菜单上,这个路由里的按钮的颜色等,此时可以使用meta来存储这些额外的信息
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/pages/Home.vue'
const router = createRouter({
history:createWebHistory(),
routes:[
...
{
name:'guanyu',
path:'/about',
component:() => import('@/pages/About.vue'),
// 使用meta添加额外信息
meta: {
isShow: false,
color: '#fff'
}
}
]
})
export default router
About
组件
<template>
{{route.meta}}
</template>
<script setup>
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印meta信息
console.log(route.meta)
</script>
6.11. 【重定向】
作用:将特定的路径,重新定向到已有路由。
具体编码:
{
path:'/',
redirect:'/about'
}
6.12. 【 replace属性】
作用:控制路由跳转时操作浏览器历史记录的模式。
浏览器的历史记录有两种写入方式:分别为
push
和replace
:push
是追加历史记录(默认值)此时点击返回会返回到从那个页面进来的url。replace
是替换当前记录。
开启
replace
模式:<router-link to="/news" active-class="active" replace>新闻</router-link>
6.13. 【编程式导航】
push和replace
路由组件的两个重要的属性:$route
和$router
变成了两个hooks
import {useRoute,useRouter} from 'vue-router'
const route = useRoute()
const router = useRouter()
console.log(route.query)
console.log(route.parmas)
console.log(route.meta)
// 获取所有路由信息
console.log(router.getRoutes())
router.push
是追加历史记录 router.replace
是替换当前记录,传递的参数都和router-link
中to的对象写法一样
以下是router.push
的代码:(下面有完整代码)
router.push({
name: 'xiang',
params: {
id: 3,
title: '新闻3',
content: '内容3'
}
})
点击新闻1
、新闻2
后调用编程式导航的push跳到新闻3
,此时再返回回到了新闻2

以下是router.replace
的代码:(下面有完整代码)
router.replace({
name: 'xiang',
params: {
id: 3,
title: '新闻3',
content: '内容3'
}
})
点击新闻1
、新闻2
后调用编程式导航的replace跳到新闻3
,新闻3
会替换掉新闻2
,此时再返回就回到了新闻1

路由的前进与后退
back和forward与浏览器的前进和后退功能一样
// 后退
router.back()
// 前进
router.forward()

router.go(-1)
和router.back()
功能一样,router.go(1)
和router.forward()
功能一样,router.go(0)
和浏览器的刷新按钮功能一样
// go后退1次
router.go(-1)
// go刷新
router.go(0)
// go前进1次
router.go(1)
// go前进2次
router.go(2)

News
组件完整代码:
<template>
<div class="news">
<nav class="news-list">
<!-- 跳转并携带params参数(to的对象写法) -->
<div v-for="news in newsList" :key="news.id">
<router-link
:to="{
name:'xiang', //用name跳转 (使用对象写法 to中传递params参数 不能使用path只能使用name)
params:{
id:news.id,
title:news.title,
content:news.content
}
}"
>
{{ news.title }}
</router-link>
</div>
<!-- push模式 -->
<button @click="push">push</button>
<button @click="replace">replace</button>
<button @click="()=>router.back()">back后退</button>
<button @click="()=>router.forward()">forward前进</button>
<button @click="()=>router.go(-1)">go后退1次</button>
<button @click="()=>router.go(0)">go刷新</button>
<button @click="()=>router.go(1)">go前进1次</button>
<button @click="()=>router.go(2)">go前进2次</button>
</nav>
<div class="news-detail">
<router-view/>
</div>
</div>
</template>
<script setup lang="ts">
import {reactive} from "vue"
import router from "@/router";
const newsList = reactive([
{id: 1, title: '新闻1', content: '内容1'},
{id: 2, title: '新闻2', content: '内容2'},
{id: 3, title: '新闻3', content: '内容3'},
{id: 4, title: '新闻4', content: '内容4'},
])
const push = () => {
router.push({
name: 'xiang',
params: {
id: 3,
title: '新闻3',
content: '内容3'
}
})
}
const replace = () => {
router.replace({
name: 'xiang',
params: {
id: 3,
title: '新闻3',
content: '内容3'
}
})
}
</script>
6.14 动态路由
src/router/index.ts
添加路由
router.addRoute({
name: 'me',
path: '/me',
component: () => import('@/pages/Me.vue')
})
console.log(router.getRoutes())

使用这种方式可以添加路由,但是如果是从服务器返回来的数据,再添加到路由中,就会出现如下的问题,这是因为再打包时发现没有引用Me.vue
这个组件,因此并不会将其打包,从而导致找不到组件
const route = JSON.parse('{"name":"me","path":"/me","component":"/src/pages/Me.vue"}')
router.addRoute({
name: route.name,
path: route.path,
component: () => import(route.component)
})
console.log(router.getRoutes())

我们可以使用import.meta.glob
将/src/pages
包及其子包的vue文件都导入进来,这样就可以使用动态路由了
const modules = import.meta.glob('/src/pages/**/**.vue')
const route = JSON.parse('{"name":"me","path":"/me","component":"/src/pages/Me.vue"}')
router.addRoute({
name: route.name,
path: route.path,
component: modules[route.component]
})
console.log(router.getRoutes())
删除路由
有几个不同的方法来删除现有的路由:
通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:
router.addRoute({ path: '/about', name: 'about', component: About }) // 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的 router.addRoute({ path: '/other', name: 'about', component: Other })
通过调用
router.addRoute()
返回的回调:const removeRoute = router.addRoute(routeRecord) removeRoute() // 删除路由如果存在的话
当路由没有名称时,这很有用。
通过使用
router.removeRoute()
按名称删除路由:router.addRoute({ path: '/about', name: 'about', component: About }) // 删除路由 router.removeRoute('about')
需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用
Symbol
作为名字。
当路由被删除时,所有的别名和子路由也会被同时删除
添加嵌套路由
要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute()
,这将有效地添加路由,就像通过 children
添加的一样:
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })
这等效于:
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
})
查看现有路由
Vue Router 提供了两个功能来查看现有的路由:
router.hasRoute()
:检查路由是否存在。router.getRoutes()
:获取一个包含所有路由记录的数组。
6.15 路由守卫
路由守卫官方写的挺好的,下面大部分抄的官方的
全局前置守卫
你可以使用 router.beforeEach
注册一个全局前置守卫:
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。
每个守卫方法接收两个参数:
to
: 即将要进入的目标from
: 当前导航正要离开的路由
可以返回的值如下:
false
: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from
路由对应的地址。- 一个路由地址: 通过一个路由地址重定向到一个不同的地址,如同调用
router.push()
,且可以传入诸如replace: true
或name: 'home'
之类的选项。它会中断当前的导航,同时用相同的from
创建一个新导航。
router.beforeEach(async (to, from) => {
if (
// 检查用户是否已登录
!isAuthenticated &&
// ❗️ 避免无限重定向
to.name !== 'Login'
) {
// 将用户重定向到登录页面
return { name: 'Login' }
}
})
如果遇到了意料之外的情况,可能会抛出一个 Error
。这会取消导航并且调用 router.onError()
注册过的回调。
如果什么都没有,undefined
或返回 true
,则导航是有效的,并调用下一个导航守卫
以上所有都同 async
函数 和 Promise 工作方式一样:
router.beforeEach(async (to, from) => {
// canUserAccess() 返回 `true` 或 `false`
const canAccess = await canUserAccess(to)
if (!canAccess) return '/login'
})
可选的第三个参数 next
在之前的 Vue Router 版本中,还可以使用 第三个参数 next
。这是一个常见的错误来源,我们经过 RFC 讨论将其移除。然而,它仍然是被支持的,这意味着你可以向任何导航守卫传递第三个参数。在这种情况下,确保 next
在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到/login
的错误用例:
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// 如果用户未能验证身份,则 `next` 会被调用两次
next()
})
下面是正确的版本:
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
全局解析守卫
你可以用 router.beforeResolve
注册一个全局守卫。这和 router.beforeEach
类似,因为它在每次导航时都会触发,不同的是,解析守卫刚好会在导航被确认之前、所有组件内守卫 (beforeRouteEnter,后面会讲) 和异步路由组件被解析之后调用。这里有一个例子,确保用户可以访问自定义 meta 属性 requiresCamera
的路由:
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
router.beforeResolve
是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
全局后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next
函数也不会改变导航本身:
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})
它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
它们也反映了 navigation failures 作为第三个参数:
router.afterEach((to, from, failure) => {
if (!failure) sendToAnalytics(to.fullPath)
})
了解更多关于 navigation failures 的信息在它的指南中。
在守卫内的全局注入
从 Vue 3.3 开始,你可以在导航守卫内使用 inject()
方法。这在注入像 pinia stores 这样的全局属性时很有用。在 app.provide()
中提供的所有内容都可以在 router.beforeEach()
、router.beforeResolve()
、router.afterEach()
内获取到:
// main.ts
const app = createApp(App)
app.provide('global', 'hello injections')
// router.ts or main.ts
router.beforeEach((to, from) => {
const global = inject('global') // 'hello injections'
// a pinia store
const userStore = useAuthStore()
// ...
})
路由独享的守卫
你可以直接在路由配置上定义 beforeEnter
守卫:
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
beforeEnter
守卫 只在进入路由时触发,不会在 params
、query
或 hash
改变时触发。例如,从 /users/2
进入到 /users/3
或者从 /users/2#info
进入到 /users/2#projects
都不会触发。它们只有在 从一个不同的 路由导航时,才会被触发。
你也可以将一个函数数组传递给 beforeEnter
,这在为不同的路由重用守卫时很有用:
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
请注意,你也可以通过使用路径 meta 字段和全局导航守卫来实现类似的行为。
组件内的守卫
最后,你可以在路由组件内直接定义路由导航守卫(传递给路由配置的)
可用的配置 API
你可以为路由组件添加以下配置:
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
beforeRouteEnter
守卫 不能 访问 this
,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
注意 beforeRouteEnter
是支持给 next
传递回调的唯一守卫。对于 beforeRouteUpdate
和 beforeRouteLeave
来说,this
已经可用了,所以不支持 传递回调,因为没有必要了:
beforeRouteUpdate (to, from) {
// just use `this`
this.name = to.params.name
}
这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回 false
来取消。
beforeRouteLeave (to, from) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (!answer) return false
}
使用组合式 API
你可以通过 onBeforeRouteUpdate
和 onBeforeRouteLeave
分别添加 update 和 leave 守卫,组合式API没有enter守卫,可以使用如下方式使用enter守卫。
<script>
import {defineComponent, ref} from "vue"
const data = ref(false)
export default defineComponent({
name: "About",
beforeRouteEnter(to, from, next) {
data.value = true
// 不能使用setup里定义的变量
// data2.value = false
next();
},
})
</script>
<script setup>
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router"
// 普通的script里导入了ref,这里就不用再导入ref了
const data2 = ref(true)
onBeforeRouteUpdate((to, from, next) => {
next()
})
onBeforeRouteLeave((to, from, next) => {
// setup语法糖里的可以直接使用别的普通script里的变量
data.value = false
data2.value = true
next()
})
</script>
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
6.16 缓存路由
缓存组件
官方写的挺好的,这里直接抄的官方的
<KeepAlive>
是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。
基本使用
在组件基础章节中,我们已经介绍了通过特殊的 <component>
元素来实现动态组件的用法:
<component :is="activeComponent" />
默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
在下面的例子中,你会看到两个有状态的组件——A 有一个计数器,而 B 有一个通过 v-model
同步 input 框输入内容的文字展示。尝试先更改一下任意一个组件的状态,然后切走,再切回来:
你会发现在切回来之后,之前已更改的状态都被重置了。
在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被“切走”的时候保留它们的状态。要解决这个问题,我们可以用 <KeepAlive>
内置组件将这些动态组件包装起来:
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
现在,在组件切换时状态也能被保留了:
在 DOM 内模板中使用时,它应该被写为
<keep-alive>
。
包含/排除
<KeepAlive>
默认会缓存内部的所有组件实例,但我们可以通过 include
和 exclude
prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="view" />
</KeepAlive>
<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
<component :is="view" />
</KeepAlive>
<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
<component :is="view" />
</KeepAlive>
它会根据组件的 name
选项进行匹配,所以组件如果想要条件性地被 KeepAlive
缓存,就必须显式声明一个 name
选项。
在 3.2.34 或以上的版本中,使用
<script setup>
的单文件组件会自动根据文件名生成对应的name
选项,无需再手动声明。
最大缓存实例数
我们可以通过传入 max
prop 来限制可被缓存的最大组件实例数。<KeepAlive>
的行为在指定了 max
后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
template
<KeepAlive :max="10">
<component :is="activeComponent" />
</KeepAlive>
缓存实例的生命周期
当一个组件实例从 DOM 上移除但因为被 <KeepAlive>
缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
一个持续存在的组件可以通过 onActivated()
和 onDeactivated()
注册相应的两个状态的生命周期钩子:
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
请注意:
onActivated
在组件挂载时也会调用,并且onDeactivated
在组件卸载时也会调用。- 这两个钩子不仅适用于
<KeepAlive>
缓存的根组件,也适用于缓存树中的后代组件。
缓存路由
RouterView 插槽
RotuerView 组件暴露了一个插槽,可以用来渲染路由组件:
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
上面的代码等价于不带插槽的 <router-view />
,但是当我们想要获得其他功能时,插槽提供了额外的扩展性。
KeepAlive & Transition
当在处理 KeepAlive 组件时,我们通常想要保持路由组件活跃,而不是 RouterView 本身。为了实现这个目的,我们可以将 KeepAlive 组件放置在插槽内:
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
类似地,插槽允许我们使用一个 Transition 组件来实现在路由组件之间切换时实现过渡效果:
<router-view v-slot="{ Component }">
<transition>
<component :is="Component" />
</transition>
</router-view>
我们也可以在 Transition 组件内使用 KeepAlive 组件:
<router-view v-slot="{ Component }">
<transition>
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
关于更多 RouterView 组件和 Transition 组件之间的互动,请参考 Transitions 指南。
传递 props 和插槽
我们可以利用其插槽给路由组件传递 props 或插槽:
<router-view v-slot="{ Component }">
<component :is="Component" some-prop="a value">
<p>Some slotted content</p>
</component>
</router-view>
实践中通常不会这么做,因为这样会导致所有路由组件都使用相同的 props 和插槽。请查阅传递 props 给路由组件获取其他传递 props 的方式。
模板引用
使用插槽可以让我们直接将模板引用放置在路由组件上:
<router-view v-slot="{ Component }">
<component :is="Component" ref="mainContent" />
</router-view>
而如果我们将引用放在 <router-view>
上,那引用将会被 RouterView 的实例填充,而不是路由组件本身。
7. pinia
7.1【准备一个效果】

7.2【搭建 pinia 环境】
第一步:npm install pinia
第二步:操作src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
/* 引入createPinia,用于创建pinia */
import { createPinia } from 'pinia'
/* 创建pinia */
const pinia = createPinia()
const app = createApp(App)
/* 使用插件 */{}
app.use(pinia)
app.mount('#app')
此时开发者工具中已经有了pinia
选项

7.3【存储+读取数据】
Store
是一个保存:状态、业务逻辑 的实体,每个组件都可以读取、写入它。它有三个概念:
state
、getter
、action
,相当于组件中的:data
、computed
和methods
。具体编码:
src/store/count.ts
// 引入defineStore用于创建store import {defineStore} from 'pinia' // 定义并暴露一个store export const useCountStore = defineStore('count',{ // 动作 actions:{}, // 状态 state(){ return { sum:6 } }, // 计算 getters:{} })
具体编码:
src/store/talk.ts
// 引入defineStore用于创建store import {defineStore} from 'pinia' // 定义并暴露一个store export const useTalkStore = defineStore('talk',{ // 动作 actions:{}, // 状态 state(){ return { talkList:[ {id:'yuysada01',content:'你今天有点怪,哪里怪?怪好看的!'}, {id:'yuysada02',content:'草莓、蓝莓、蔓越莓,你想我了没?'}, {id:'yuysada03',content:'心里给你留了一块地,我的死心塌地'} ] } }, // 计算 getters:{} })
组件中使用
state
中的数据<template> <h2>当前求和为:{{ sumStore.sum }}</h2> </template> <script setup lang="ts" name="Count"> // 引入对应的useXxxxxStore import {useSumStore} from '@/store/sum' // 调用useXxxxxStore得到对应的store const sumStore = useSumStore() </script>
<template> <ul> <li v-for="talk in talkStore.talkList" :key="talk.id"> {{ talk.content }} </li> </ul> </template> <script setup lang="ts" name="Count"> import axios from 'axios' import {useTalkStore} from '@/store/talk' const talkStore = useTalkStore() </script>
7.4.【修改数据】(三种方式)
第一种修改方式,直接修改
countStore.sum = 666
第二种修改方式:批量修改
countStore.$patch({ sum:999, school:'atguigu' })
第三种修改方式:借助
action
修改(action
中可以编写一些业务逻辑)import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { /*************/ actions: { //加 increment(value:number) { if (this.sum < 10) { //操作countStore中的sum this.sum += value } }, //减 decrement(value:number){ if(this.sum > 1){ this.sum -= value } } }, /*************/ })
组件中调用
action
即可// 使用countStore const countStore = useCountStore() // 调用对应action countStore.incrementOdd(n.value)
7.5.【storeToRefs】
- 借助
storeToRefs
将store
中的数据转为ref
对象,方便在模板中使用。 - 注意:
pinia
提供的storeToRefs
只会将数据做转换,而Vue
的toRefs
会转换store
中数据。
<template>
<div class="count">
<h2>当前求和为:{{sum}}</h2>
</div>
</template>
<script setup lang="ts" name="Count">
import { useCountStore } from '@/store/count'
/* 引入storeToRefs */
import { storeToRefs } from 'pinia'
/* 得到countStore */
const countStore = useCountStore()
/* 使用storeToRefs转换countStore,随后解构 */
const {sum} = storeToRefs(countStore)
</script>
7.6.【getters】
概念:当
state
中的数据,需要经过处理后再使用时,可以使用getters
配置。追加
getters
配置。// 引入defineStore用于创建store import {defineStore} from 'pinia' // 定义并暴露一个store export const useCountStore = defineStore('count',{ // 动作 actions:{ /************/ }, // 状态 state(){ return { sum:1, school:'atguigu' } }, // 计算 getters:{ bigSum:(state):number => state.sum *10, upperSchool():string{ return this. school.toUpperCase() } } })
组件中读取数据:
const {increment,decrement} = countStore let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)
7.7.【$subscribe】
通过 store 的 $subscribe()
方法侦听 state
及其变化
talkStore.$subscribe((mutate,state)=>{
console.log('LoveTalk',mutate,state)
localStorage.setItem('talk',JSON.stringify(talkList.value))
})
7.8. 【store组合式写法】
import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'
import {reactive} from 'vue'
export const useTalkStore = defineStore('talk',()=>{
// talkList就是state
const talkList = reactive(
JSON.parse(localStorage.getItem('talkList') as string) || []
)
// getATalk函数相当于action
async function getATalk(){
// 发请求,下面这行的写法是:连续解构赋值+重命名
let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
// 把请求回来的字符串,包装成一个对象
let obj = {id:nanoid(),title}
// 放到数组中
talkList.unshift(obj)
}
return {talkList,getATalk}
})
defineAsyncComponent
defineExpose
onRouteLeave
8. 组件通信
① 父传子
使用props只能查看数据,不能修改数据。
父组件传给子组件的数据,当该数据在父组件的中发生改变的时候,子组件中的数据不会因其的改变而去改变。是因为父组件传给子组件的数据是通过子组件的mounted阶段进行的处理,才能通过data渲染到页面上,从而更新。但是数据发生变化的时候,mounted阶段已经过了,所以data捕获不到数据的更新,从而无法更新。
参考链接:https://www.jb51.net/article/265304.htm
1、父组件传递方式
<template>
<div class="hello">
我是父组件
<!-- 父组件通过:变量(这里是info)绑定值 -->
<Child :info="parentMsg" :bar="1"></Child>
</div>
</template>
<script setup>
import Child from './Child.vue'
import {ref} from 'vue'
const parentMsg=ref('父组件传递值是a')
</script>
<style scoped>
</style>
2、子组件接收方式和使用
<template>
<!-- info是父组件传递过了的值 -->
<div>我是子组件拿到了父组件info的值是=>{{info}}</div>
<div>我是子组件拿到了父组件props.bar的值是=>{{props.bar}}</div>
<!-- <template>可以直接使用pros里的数据(<script>里不行) -->
<div>我是子组件拿到了父组件bar的值是=>{{bar}}</div>
<button @click="alertBar">点击</button>
</template>
<script setup>
//defineProps是Vue3提供方法,不需要引入直接使用(当然引入了也不会报错)
import { toRefs, defineProps } from 'vue'
const props = defineProps({
//子组件接收父组件传递过来的值
info: String,
bar: {
type: Number, //参数的类型
required: false, //是否是必须的参数
default: 0, //默认数据
}
})
//使用父组件传递过来的值(不添加这一行也行)
const {info} =toRefs(props)
const alertBar = ()=>{
// console.log(bar); // bar is not defined
console.log(props.bar) // 在<script>里必须带上props,在<template>可以直接使用pros里的数据
alert(props.bar)
}
</script>
3、效果图

② 子传父
vue3中子组件向父组件传递值和vue2.x的区别是vue2.x使用的是 $emit 而vue3使用的是emit,它们的传值一样都是方法加值,即vue2.x的是this.$emit('方法名','传递的值(根据需要传或者不传)'),vue3的setup语法糖的是defineEmits。vue3的子传父方式如下所示:
1、子组件的传递方式
<template>
<div>
<button @click="clickChild">点击子组件</button>
</div>
</template>
<script setup>
// 使用defineEmits创建名称,接受一个数组
const emit = defineEmits(['clickChild','click'])
const clickChild = () => {
let param1 = { content: 'b' }
//传递给父组件
emit('clickChild', param1)
let param2 = { content: 'c' }
emit('click', param2)
}
</script>
2、父组件接收与使用
<Child @click="clickEven"></Child>
vue2中这种写法是自定义事件,可以通过.native修饰符变为原生DOM事件 vue3中直接写则为原生DOM事件;如果在子组件使用
defineEmits(['clickChild','click'])
定义了click
并且使用emit('click', param2)
则为自定义事件;如果直接为defineEmits(['clickChild'])
并且使用emit('click', param2)
则为自定义事件,不过会报警告
<template>
<div class="hello">
<h1>我是父组件</h1>
<!-- clickChild是子组件绑定的事件,click是父组件接受方式 -->
<Child @clickChild="clickEven" @click="clickMsg"></Child>
<h1>子组件传递的值是 {{result}}</h1>
</div>
</template>
<script setup>
import Child from './Child.vue'
import {ref} from 'vue'
const result=ref('')
const clickEven=(param1)=>{
console.log(param1);
result.value=param1.content
}
const clickMsg=(param2)=>{
console.log(param2);
}
</script>
3、效果图

③ 定义暴露属性和方法(非常常用)
使用这种方式既可以查看数据也能修改数据
父组件获取并修改子组件数据和方法
当时用语法糖时,需要将组建的属性及方法通过defineExpose导出,父组件才能访问到数据,否则拿不到子组件的数据
父组件
父组件在使用子组件时可以添加ref="testRef"
,并且定义const testRef = ref()
,这样就
<template>
<div class="hello">
<h1>我是父组件</h1>
<Child ref="testRef"></Child>
<button @click="getSonHander">获取子组件中的数据</button>
</div>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const testRef = ref()
const getSonHander = () => {
console.log('获取子组件中的性别', testRef.value.sex);
console.log('获取子组件中的其他信息', testRef.value.info)
testRef.value.info.age += 1
console.log("修改子组件的年龄",testRef.value.info.age);
console.log("调用子组件的方法==>");
testRef.value.say()
}
</script>
子组件
<template>
<div>
<div> 我是子组件 </div>
<div>性别:{{ sex }}</div>
</div>
</template>
<script setup>
import { reactive, ref, defineExpose } from "vue";
let sex = ref('男')
let info = reactive({ like: '王者荣耀', age: 18 })
let say = () => {
console.log("我是子组件");
}
defineExpose({ sex, info, say })
</script>
效果图

子组件获取并修改父组件属性和方法
父组件
<template>
<div>
<h1> 我是父组件</h1>
<div>性别:{{ sex }}</div>
<Child></Child>
</div>
</template>
<script setup>
import { reactive, ref, defineExpose } from "vue";
import Child from './Child.vue'
let sex = ref('男')
let info = reactive({ like: '钓鱼', age: 38 })
let say = () => {
console.log("我是父组件");
}
defineExpose({ sex, info, say })
</script>
子组件
要想获得父组件实例vc,调用方法时参数名必须为
$parent
<template>
<div class="hello">
<div>我是子组件</div>
<button @click="getFather($parent)">获取父组件中的数据</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const testRef = ref()
const getFather = ($parent) => {
console.log('获取父组件中的性别', $parent.sex);
console.log('获取父组件中的其他信息', $parent.info)
$parent.info.age += 1
console.log("修改父组件的年龄",$parent.info.age);
console.log("调用父组件的方法==>");
$parent.say()
}
</script>

④ 全局事件总线
全局事件总线可以实现任意组件通信,在vue2中可以根据VM与VC关系推出全局事件总线。
但是在vue3中没有Vue构造函数,也就没有Vue.prototype.以及组合式API写法没有this,
那么在Vue3想实现全局事件的总线功能就有点不现实啦,如果想在Vue3中使用全局事件总线功能,可以使用插件mitt实现。
mitt:官网地址:https://www.npmjs.com/package/mitt
安装
npm install --save mitt
src\bus\index.ts
//引入mitt插件:mitt一个方法,方法执行会返回bus对象
import mitt from 'mitt';
const $bus = mitt();
export default $bus;
组件1:
<template>
<h3>我是子组件1:曹植</h3>
</template>
<script setup lang="ts">
import $bus from "@/bus";
//组合式API函数
import { onMounted } from "vue";
//组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据
onMounted(() => {
//第一个参数:即为事件类型 第二个参数:即为事件回调
$bus.on("car", (car) => {
console.log(car);
});
});
</script>
组件2:
<template>
<h2>我是子组件2:曹丕</h2>
<button @click="handler">点击我给兄弟送一台法拉利</button>
</template>
<script setup lang="ts">
//引入$bus对象
import $bus from '@/bus';
//点击按钮回调
const handler = () => {
$bus.emit('car', { car: "法拉利" });
}
</script>
⑤ v-model父子组件通讯
传统父子组件通讯方式
传统方式我们需要在父组件中定义一个let money = ref(10000);
,然后在使用子组件的标签上添加:modelValue="money"
绑定给子组件,并且使用@update:modelValue
接收子组件的自定义事件(这里的自定义事件名字叫update:modelValue
,中间的:
仅仅做分割作用,没有什么特殊的含义),然后指定这个自定义事件的处理函数为handler
,并在handler
方法里添加修改money
的值
<template>
<div>
<h1>v-model:钱数{{ money }}</h1>
<Child :modelValue="money" @update:modelValue="handler"></Child>
</div>
</template>
<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
//父组件的数据钱数
let money = ref(10000);
//自定义事件的回调
const handler = (num) => {
//将来接受子组件传递过来的数据
money.value = num;
};
</script>
在子组件里,我们需要使用let props = defineProps(["modelValue"])
接收父组件传递的值,使用let $emit = defineEmits(['update:modelValue'])
声明自定义事件,然后使用$emit('update:modelValue',props.modelValue+1000)
触发自定义事件以改变modelValue
(money
)的值
<template>
<div class="child">
<h3>钱数:{{ modelValue }}</h3>
<button @click="handler">父子组件数据同步</button>
</div>
</template>
<script setup>
//接受props
let props = defineProps(["modelValue"]);
let $emit = defineEmits(['update:modelValue']);
//子组件内部按钮的点击回调
const handler = ()=>{
//触发自定义事件
$emit('update:modelValue',props.modelValue+1000);
}
</script>
v-model方式
想使用v-model
的方式,我们可以删掉:modelValue="money" @update:modelValue="handler"
,修改为v-model="money"
就行了,这个父组件的handler
方法也可以删掉了
使用
v-model
来实现父子组件传递数据时,会给子组件传递一个名为modelValue
的props
,子组件可以使用let props = defineProps(["modelValue"])
接收这个props
,如果我们想要修改数据,自定义事件必须声明为let $emit = defineEmits(['update:modelValue'])
(也就是update:
+props里的名字
),然后使用$emit('update:modelValue',props.modelValue+1000)
触发更新操作,此时就会将这个触发自定义事件的第二个参数的值(也就是props.modelValue+1000
)赋给modelValue
对应的变量(也就是父组件的money
)
<template>
<div>
<h1>v-model:钱数{{ money }}</h1>
<!-- <Child :modelValue="money" @update:modelValue="handler"></Child> -->
<Child v-model="money"></Child>
</div>
</template>
<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
//父组件的数据钱数
let money = ref(10000);
//自定义事件的回调
// const handler = (num) => {
// //将来接受子组件传递过来的数据
// money.value = num;
// };
</script>
子组件不需要做任何改变
<template>
<div class="child">
<h3>钱数:{{ modelValue }}</h3>
<button @click="handler">父子组件数据同步</button>
</div>
</template>
<script setup>
//接受props
let props = defineProps(["modelValue"]);
let $emit = defineEmits(['update:modelValue']);
//子组件内部按钮的点击回调
const handler = ()=>{
//触发自定义事件
$emit('update:modelValue',props.modelValue+1000);
}
</script>
v-model绑定多个数据
如果想要绑定多个数据,可以使用v-model:pageNo="pageNo"
,也就是v-model:
+自定义名字
<template>
<div>
<h2>当前页:{{pageNo}} 每页大小:{{pageSize}}</h2>
<Child2 v-model:pageNo="pageNo" v-model:pageSize="pageSize"></Child2>
</div>
</template>
<script setup>
//引入子组件
import { ref } from "vue";
import Child2 from "./Child2.vue";
//父亲的数据
let pageNo = ref(1);
let pageSize = ref(3);
</script>
子组件使用let props = defineProps(["pageNo", "pageSize"])
接收父组件传过来的两个自定义事件,然后使用let $emit = defineEmits(["update:pageNo", "update:pageSize"])
来声明自定义事件(也就是update:
+自定义名称
),然后使用$emit("update:pageNo", props.pageNo + 3)
和$emit('update:pageSize', pageSize + 4)
更新相应数据的值
<template>
<div class="child2">
<h1>同时绑定多个v-model</h1>
<button @click="handler">pageNo{{ pageNo }}</button>
<button @click="$emit('update:pageSize', pageSize + 4)">
pageSize{{ pageSize }}
</button>
</div>
</template>
<script setup>
let props = defineProps(["pageNo", "pageSize"]);
let $emit = defineEmits(["update:pageNo", "update:pageSize"]);
//第一个按钮的事件回调
const handler = () => {
$emit("update:pageNo", props.pageNo + 3);
};
</script>
⑥ useAttrs(扩充原组件功能)
父组件
注意要先安装
mybatis-plus
并在src\main.js
中使用ElementPlus
// 引入实例化上下文的api方法createApp import { createApp } from 'vue' // 引入element ui import ElementPlus from 'element-plus' // 引入element ui的样式 import 'element-plus/dist/index.css' // 引入App组件 import App from './App.vue' // 创建app const app = createApp(App) app.use(ElementPlus) // 使用element-ui // 挂载 app.mount('#app');
<template>
<div>
<h1>useAttrs</h1>
<el-button type="primary" size="small" :icon="Edit"></el-button>
<!-- 自定义组件 -->
<HintButton type="primary" size="small" :icon="Edit" title="编辑按钮"></HintButton>
</div>
</template>
<script setup>
import { Edit } from "@element-plus/icons-vue";
import HintButton from "./HintButton.vue";
</script>
优化前的子组件
解释一下
<el-button :="$attrs"></el-button>
是什么意思:我们使用
<el-button v-bind:type="$attrs.type" v-bind:size="$attrs.size"></el-button>
这种方式可以简化为
<el-button v-bind="{ type: $attrs.type, size: $attrs.size }"></el-button>
,然后又可以简化为
<el-button v-bind="$attrs"></el-button>
,然后又可以简化为
<el-button :="$attrs"></el-button>
,这样我们就继承了el-button
所有可能的配置
<template>
<div :title="$attrs.title">
<el-button :="$attrs"></el-button>
</div>
</template>
<script setup>
//vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件!!!
//引入useAttrs方法:获取组件标签身上属性与事件
import {useAttrs} from 'vue';
//此方法执行会返回一个对象
let $attrs = useAttrs();
</script>
优化后的子组件
我们可以使用defineProps
接收自己定义的props
,由于defineProps
优先级比useAttrs
优先级高,因此我们定义的title
会存在props
里、并且useAttrs()
里就没有title
了
<template>
<div :title="title">
<el-button :="$attrs"></el-button>
</div>
</template>
<script setup>
//引入useAttrs方法:获取组件标签身上属性与事件
import {useAttrs} from 'vue';
//此方法执行会返回一个对象
let $attrs = useAttrs();
//props与useAttrs方法都可以获取父组件传递过来的属性与属性值
//但是props接受了useAttrs方法就获取不到了
let props =defineProps(['title']);
</script>
vue3框架提供的useAttrs()
方法,它可以获取组件身上的属性与事件,因此我们输出useAttrs()
能看到这些属性和事件
<template>
<div>
<h1>useAttrs</h1>
<el-button type="primary" size="small" :icon="Edit"></el-button>
<!-- 自定义组件 -->
<HintButton type="primary" size="small" :icon="Edit" title="编辑按钮" @click="handler" @xxx="handler"></HintButton>
</div>
</template>
<script setup>
//vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件!!!
//图标组件
import { Edit } from "@element-plus/icons-vue";
import HintButton from "./HintButton.vue";
//按钮点击的回调
const handler = ()=>{
alert(12306);
}
</script>
在HintButton
组件里输出$attrs
可以看到即显示了size
属性和type
属性(我们自定义的title
被放在了props
里了),又显示了onClick
事件和OnXxx
事件

provide
与 inject
(隔代组件通讯)
⑦ 使用
provide
和inject
允许隔代获取数据,也可以隔代修改数据

父组件有一个 provide("name",value)
选项来提供数据,后代组件有一个 inject("name")
选项来开始使用这些数据
祖父组件
<template>
<div class="box">
<h1>我是祖父组件,我的代步工具是:{{car}}</h1>
<hr />
<Child></Child>
</div>
</template>
<script setup>
import Parent from "./Parent.vue";
//vue3提供provide(提供)与inject(注入),可以实现隔辈组件传递数据
import { ref, provide } from "vue";
let car = ref("法拉利");
//祖先组件给后代组件提供数据
//两个参数:第一个参数就是提供的数据key
//第二个参数:祖先组件提供数据
provide("TOKEN", car);
</script>
父组件
<template>
<div>
<h2>我是父组件</h2>
<Child></Child>
</div>
</template>
<script setup>
import Child from './Child.vue';
</script>
子组件
<template>
<div>
<h3>我是孙子组件</h3>
<p>我的代步工具是:{{car}}</p>
<button @click="updateCar">败家</button>
</div>
</template>
<script setup>
import {inject} from 'vue';
//注入祖先组件提供数据
//需要参数:即为祖先提供数据的key
let car = inject('TOKEN');
const updateCar = ()=>{
car.value = '自行车';
}
</script>
点击败家之前:

点击败家之后

⑧ Pinia(组合式)
安装
npm install pinia
src/sotre/index.js
// 大仓库
import { createPinia } from 'pinia'
// 创建大仓库
const pinia = createPinia()
// 对外暴露
export default pinia
src\main.js
// 引入pinia
import pinia from './sotre'
app.use(pinia)
src\store\modules\todo.js
组合式与选择式的方式区别主要是在仓库的写法上,别的没什么不同。仓库的写法非常像vue文件里组合式API的写法
//定义组合式API仓库
import { defineStore } from "pinia";
import { ref, computed,watch} from 'vue';
//创建小仓库
let useTodoStore = defineStore('todo', () => {
let arr = ref([1,2,3,4,5]);
const total = computed(() => {
return arr.value.reduce((prev, next) => {
return prev + next;
}, 0)
})
//务必要返回一个对象:属性与方法可以提供给组件使用
return {
arr,
total,
updateTodo() {
arr.value.push(1);
}
}
});
export default useTodoStore;
使用:
<template>
<div>
<div>数组:{{ todoStore.arr }}</div>
<div>长度:{{todoStore.total}}</div>
<button @click="updateTodo">点我更新数据</button>
</div>
</template>
<script setup>
//引入组合式API函数仓库
import useTodoStore from "@/store/modules/todo";
let todoStore = useTodoStore();
//点击p段落去修改仓库的数据
const updateTodo = () => {
todoStore.updateTodo();
};
</script>
⑧ 插槽
默认插槽
父组件
<template>
<h1>我是父组件</h1>
<Child>
<div>子组件,你好</div>
</Child>
</template>
<script name="App" setup>
import Child from "./Child.vue";
</script>
子组件
<template>
<div>
<h2>我是子组件</h2>
<slot></slot>
</div>
</template>

具名插槽
<template>
<h1>我是父组件</h1>
<Child>
<template v-slot:msg1>
<div>子组件,你好</div>
</template>
<template #msg2>
<div>子组件,你好!!!</div>
</template>
</Child>
</template>
<script name="App" setup>
import Child from "./Child.vue";
</script>
<template>
<div>
<h2>我是子组件</h2>
<slot name="msg1"></slot>
<slot name="msg2"></slot>
</div>
</template>

作用域插槽
作用域插槽:就是可以传递数据的插槽,子组件可以将数据回传给父组件,父组件可以决定这些回传的数据是以何种的结构或者外观在子组件内部去展示。
父组件
<template>
<h1>我是父组件</h1>
<Child :todos="todos">
<template v-slot="todo">
<h3>{{ todo }}</h3>
</template>
</Child>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const todos = ref([
{ id: 1, title: '吃饭', done: true },
{ id: 2, title: '睡觉', done: false },
{ id: 3, title: '打游戏', done: true },
])
</script>
子组件
<template>
<div>
<h3>作用域插槽</h3>
<ul>
<li :style="{color:item.done?'green':'red'}" v-for="(item,index) in todos">
<!-- {{ item.title }} -->
<!-- 作用域插槽:可以将数据传给父组件 -->
<slot :$row="item" :$index="index"></slot>
</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps(["todos"])
</script>

9. 动态/懒 加载组件
9.1、动态加载组件
vue3中不能根据字符串指定组件,is
的类型需要是组件类型
<template>
<component :is="view" :data="user" />
</template>
<script setup>
import {ref} from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const view = Math.random() > 0.5 ? Foo : Bar
// 传递props数据(其他同理)
const user = ref({username:'zhangsan'})
</script>
vue2可以使用字符串来动态指定组件,但是需要引入所有可能的组件
<template>
<component :is="view" :data="user" />
</template>
<script>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: { Foo, Bar },
data() {
return {
view: Math.random() > 0.5 ? 'Foo' : 'Bar',
user: {username:'zhangsan'}
}
}
}
</script>
如果在 <component>
标签上使用 v-model
,模板编译器会将其扩展为 modelValue
prop 和 update:modelValue
事件监听器,就像对任何其他组件一样。但是,这与原生 HTML 元素不兼容,例如 <input>
或 <select>
。因此,在动态创建的原生元素上使用 v-model
将不起作用:
<template>
<!-- 由于 'input' 是原生 HTML 元素,因此这个 v-model 不起作用 -->
<component :is="tag" v-model="username" />
{{username}}
</template>
<script setup>
import { ref } from 'vue'
const tag = ref('input')
const username = ref('tom')
</script>
9.2、异步加载组件
基本用法
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
如你所见,defineAsyncComponent
方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve
回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason)
表明加载失败。
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./Foo.vue'))
最后得到的 AsyncComp
是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
与普通组件一样,异步组件可以使用 app.component()
全局注册:
app.component('MyComponent', defineAsyncComponent(() => import('./Foo.vue')))
可以直接在父组件中直接定义它们:
<template>
<Foo />
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const Foo = defineAsyncComponent(() =>import('./Foo.vue'))
</script>
加载与错误状态
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent()
也支持在高级选项中处理这些状态:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。
【Suspense】异步组件渲染加载内容
- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
- 使用步骤:
- 异步引入组件
- 使用
Suspense
包裹组件,并配置好default
与fallback
import { defineAsyncComponent,Suspense } from "vue";
const Foo = defineAsyncComponent(()=>import('./Foo.vue'))
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Foo/>
</template>
<template v-slot:fallback>
<h3>加载中.......</h3>
</template>
</Suspense>
</div>
</template>
10. Vue3新功能
10.1. 【Teleport】组件指定位置
- 什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
如下是将弹窗放到body里的示例:
<teleport to='body' >
<div class="modal" v-show="isShow">
<h2>我是一个弹窗</h2>
<p>我是弹窗中的一些内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
10.2.【全局API转移到应用对象】
app.component
app.config
app.directive
app.mount
app.unmount
app.use
10.3.【其他】
过渡类名
v-enter
修改为v-enter-from
、过渡类名v-leave
修改为v-leave-from
。keyCode
作为v-on
修饰符的支持。v-model
指令在组件上的使用已经被重新设计,替换掉了v-bind.sync。
v-if
和v-for
在同一个元素身上使用时的优先级发生了变化。移除了
$on
、$off
和$once
实例方法。移除了过滤器
filter
。移除了
$children
实例propert
。......
11. 其它 API(很少使用)
11.1.【shallowRef 与 shallowReactive 】
shallowRef
作用:创建一个响应式数据,但只对顶层属性进行响应式处理。
用法:
let myVar = shallowRef(initialValue);
特点:只跟踪引用值的变化,不关心值内部的属性变化。
shallowReactive
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
用法:
const myObj = shallowReactive({ ... });
特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。
总结
通过使用
shallowRef()
和shallowReactive()
来绕开深度响应。浅层式API
创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
11.2.【readonly 与 shallowReadonly】
readonly
作用:用于创建一个对象的深只读副本。
用法:
const original = reactive({ ... }); const readOnlyCopy = readonly(original);
特点:
- 对象的所有嵌套属性都将变为只读。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
shallowReadonly
作用:与
readonly
类似,但只作用于对象的顶层属性。用法:
const original = reactive({ ... }); const shallowReadOnlyCopy = shallowReadonly(original);
特点:
只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
适用于只需保护对象顶层属性的场景。
11.3.【toRaw 与 markRaw】
toRaw
作用:用于获取一个响应式对象的原始对象,
toRaw
返回的对象不再是响应式的,不会触发视图更新。官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
何时使用? —— 在需要将响应式对象传递给非
Vue
的库或外部系统时,使用toRaw
可以确保它们收到的是普通对象具体编码:
import { reactive,toRaw,markRaw,isReactive } from "vue"; /* toRaw */ // 响应式对象 let person = reactive({name:'tony',age:18}) // 原始对象 let rawPerson = toRaw(person) /* markRaw */ let citysd = markRaw([ {id:'asdda01',name:'北京'}, {id:'asdda02',name:'上海'}, {id:'asdda03',name:'天津'}, {id:'asdda04',name:'重庆'} ]) // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了 let citys2 = reactive(citys) console.log(isReactive(person)) console.log(isReactive(rawPerson)) console.log(isReactive(citys)) console.log(isReactive(citys2))
markRaw
作用:标记一个对象,使其永远不会变成响应式的。
例如使用
mockjs
时,为了防止误把mockjs
变为响应式对象,可以使用markRaw
去标记mockjs
编码:
/* markRaw */ let citys = markRaw([ {id:'asdda01',name:'北京'}, {id:'asdda02',name:'上海'}, {id:'asdda03',name:'天津'}, {id:'asdda04',name:'重庆'} ]) // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了 let citys2 = reactive(citys)