Vue3
一、Vue3 简介
- 2020年9月18日,
Vue.js发布版3.0版本,代号:One Piece
1.1 性能提升
- 打包大小减少
41%。 - 初次渲染快
55%, 更新渲染快133%。 - 内存减少
54%。
1.2 源码的升级
使用
Proxy代替defineProperty实现响应式。重写虚拟
DOM的实现和Tree-Shaking。
1.3 拥抱TypeScript
Vue3可以更好的支持TypeScript。
1.4 新特性
Composition API(组合API):setupref与reactivecomputed与watch......
新的内置组件:
FragmentTeleportSuspense......
其他改变:
新的生命周期钩子
data选项应始终被声明为一个函数移除
keyCode支持作为v-on的修饰符......
二、创建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 serve2.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? No2.2.1 自己动手编写一个App组件
mian.ts
// 引入createApp用于创建应用
import { createApp } from "vue";
// 引入App根组件
import App from "./App.vue";
createApp(App).mount("#app");App.vue
<template>
<div><h1 class="app">你好啊</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>2.2.2 总结
Vite项目中,index.html是项目的入口文件,在项目最外层;vue-cli创建的项目入口文件:main.js- 加载
index.html后,Vite解析<script type="module" src="/src/main.ts"></script>指向的TS文件 Vue3中是通过createApp函数创建一个应用实例
2.3 一个简单效果
Vue3向下兼容 Vue2 语法,且 Vue3 中的模板中可以没有根标签
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="changename">修改名字</button>
<button @click="changeage">修改年龄</button>
<button @click="showTel">查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name: "Person",
data() {
return {
name: "张三",
age: 18,
tel: "124742759548",
};
},
methods: {
showTel() {
alert(this.tel);
},
changename() {
this.name = "zhang-san";
},
changeage() {
this.age += 1;
},
},
};
</script>
<style scoped>
.person {
background-color: skyblue;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
button {
margin: 0 5px;
}
</style>Vue3的一个升级:在
<template>标签内可以不写根标签,可以有多个根标签
三、Vue3核心语法
3.1 OptionsAPI 与 CompositionAPI
Vue2的API设计是Options(配置/选项)风格的Vue3的API设计是Composition(组合)风格的
3.1.1 Options API 的弊端
Options类型的 API,数据,方法,计算属性等,是分散在:data、methods、computed中的,若想新增或者修改一个需求,就需要分别修改:data、methods、computed、不便于维护和复用。


3.1.2 Composition API 的优势
可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起


3.2 setup
3.2.1 setup 概述
setup 是 Vue3 中的一个新的配置项,值是一个函数,它是 Composition API “表演的舞台”,组件中所用到的:数据、方法、计算属性、监视.....等等,均配置在 setup 中。
特点如下:
setup函数返回的对象中的内容,可以直接在模板中使用。setup中访问this是undefined,Vue3中已经弱化了thissetup函数是组合式API的第一个钩子函数
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="showTel">查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name: "Person",
beforeCreate() {
console.log("beforeCreate");
},
// setup在beforeCreate之前执行
setup() {
console.log("setup");
// console.log(this); setup函数中的this是undefined,Vue3中已经弱化this了
// 数据,原来是写在data中的 注意:此时的name,age,tel都不是响应式的数据
let name = "张三";
let age = 18;
let tel = 121398493;
// 方法
function changeName() {
name = "zhang-san"; // 注意:这样修改name,页面是没有变化的
console.log(name); // name被修改了,但是页面没有变化
}
function changeAge() {
age += 1; //注意:这样修改name,页面是没有变化的
console.log(age); // name被修改了,但是页面没有变化
}
function showTel() {
alert(tel);
}
// 将数据和方法交出去,模板中才能直接使用
return { name, age, changeName, changeAge, showTel };
},
};
</script>
<style scoped>
.person {
background-color: skyblue;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
button {
margin: 0 5px;
}
</style>3.2.2 setup 的返回值
返回一个对象:
则返回对象中的:属性、方法等、在模板中均可以直接使用(常用)
setup(){
.....
// 将数据和方法交出去,模板中才能直接使用
return { name, age, changeName, changeAge, showTel };
}返回一个函数:
则可以在函数中自定义渲染内容
setup(){
return ()=> {
// 返回的内容会被直接渲染到页面中
return "haha";
};
}3.2.3 setup 与 Options API 的关系
setup(Vue3写法)可以和data、methods..(Vue2写法)同时存在(不建议)Vue2的配置(data、methods....)中可以通过this访问到setup中的属性、方法setup中不能访问到Vue2的配置(data、methods...)- 如果与
Vue2冲突,则setup优先
export default {
name: "Person",
data() {
return {
a: 100,
// 2.data中可以通过this读取到setup中的数据
c:this.name,
d:900
};
},
methods: {
b() {
console.log("b");
},
},
// 1.data和methods可以和setup同时存在(不建议)
setup() {
// console.log(this); this:undefined
// 数据,原来是写在data中的 注意:此时的name,age,tel都不是响应式的数据
let name = "张三";
let age = 18;
let tel = 121398493;
// 3.在setup中读取不到data中的数据
// let x = d; d未定义
// 方法
function changeName() {
name = "zhang-san"; // 注意:这样修改name,页面是没有变化的
console.log(name); // name被修改了,但是页面没有变化
}
....
// 将数据和方法交出去,模板中才能直接使用
return { name, age, changeName, changeAge, showTel };
},3.2.3 setup 语法糖
setup 函数有一个语法糖,这个语法糖,可以让我们把 setup 独立出去,代码如下:
<template>
....
</template>
// 用于指定组件在开发者工具中的名称,如果不指定默认为组件文件名
<script lang="ts">
export default {
name:'Person',
}
</script>
<!-- 下面的写法是setup语法糖 -->
<script setup lang="ts">
// 数据(注意:此时的name、age、tel都不是响应式数据)
let name = '张三'
let age = 18
let tel = '13888888888'
// 方法
function changName(){
name = '李四'//注意:此时这么修改name页面是不变化的
}
...
// 不需要使用return语句
</script>使用
<script setup lang=ts></script>包裹原本setup函数中的内容,可简化setup函数,且无需返回值
3.2.4 vite-plugin-vue-setup-extend 插件
按照上面写法,需要写两个 script 标签,一个不写 setup 用于指定组件名,一个写 setup 用于组合式API,较为麻烦。
可以借助 vite 中的插件 vite-plugin-vue-setup-extend来简化为一个 script 标签,并可以指定组件名
第一步:
npm i vite-plugin-vue-setup-extend -D第二步:配置
vite.config.ts
.....
// 1.引入
import VueSetupExtend from "vite-plugin-vue-setup-extend"
export default defineConfig({
plugins: [
vue(),
// 2.使用
VueSetupExtend(),
],
.....第三步:在
script标签中指定组件名vue<script setup lang="ts" name="Person">..</script>
3.3 ref 创建:基本类型的响应式数据
- 作用:定义基本类型响应式变量
- 语法:
<template>
<!-- 4.使用响应式数据不需要.value -->
<h2>{{ xxx }}}</h2>
</template>
<script setup lang="ts" name="Person">
// 1.引入ref
import { ref } from "vue";
// 2.使用ref函数包裹响应式数据
let xxx = ref(初始值);
// 3.使用xxx.value修改响应式数据
xxx.value = xx; // JS中操作ref对象时候需要.value
</script>- 返回值:一个
RefImpl的实例对象,简称ref响应式对象或ref、ref对象的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>注意:
JS中操作数据需要:xxx.value,但模板中不需要.value、直接使用即可- 对应
let name = ref("张三")来说、name不是响应式的、name.value是响应式的
3.4 reactive 创建:对象类型的响应式数据
- 作用:定义一个响应式对象(object)(基本类型必须使用
ref,否则报错) - 语法:
<template>
<!-- 4.使用响应式对象数据 -->
<h2>obj.name</h2>
</template>
<script setup lang="ts" name="Person">
// 1.引入reactive
import { reactive } from "vue";
// 2.定义响应式对象
let 响应式对象 = reactive(源对象);
let obj = reactive({name:"张三"})
// 3.修改响应式数据
obj.name = "李四"
</script>- 返回值:一个
Proxy的实例对象,简称:响应式对象
<template>
<div class="person">
<h2>汽车信息</h2>
<h2>一辆{{ car.brand }}车价值:{{ car.price }}w</h2>
<button @click="changePrice">修改汽车的价格</button>
<br />
<h2>游戏列表</h2>
<ul>
<li v-for="game of games" :key="game.id">{{ game.name }}</li>
</ul>
<button @click="changeFirstGame">修改第一个游戏的名字</button>
<hr />
<h2>深层次响应 {{ obj.a.b.c }}</h2>
<button @click="changeObj">测试</button>
</div>
</template>
<script setup lang="ts" name="Person">
// 引入 reactive 实现对象类型的响应式数据
import { reactive } from "vue";
// 数据 使用reactive()包裹响应式对象类型的数据
let car = reactive({ brand: "奔驰", price: 100 });
let games = reactive([
{ id: "dshj01", name: "原神" },
{ id: "dshj01", name: "和平精英" },
{ id: "dshj01", name: "王者荣耀" },
]);
// 深层次响应数据
let obj = reactive({
a: {
b: {
c: 666,
},
},
});
console.log(car); // Proxy
console.log(games);// Proxy
// 方法
function changePrice() {
car.price += 10;
}
function changeFirstGame() {
games[0].name = "绝地求生";
}
function changeObj() {
obj.a.b.c = 888;
}
</script>注意:
reactive定义的响应式数据是“深层次的”。(即对象可以多层嵌套)reactive只能定义对象类型的响应式数据- 对象类型的数据可以是:对象、数组、方法....
3.5 ref创建:对象类型的响应式数据
- 其实
ref接收的数据可以是(any):基本类型,对象类型 - 若
ref接收的是对象类型,内部其实也是调用了reactive函数 ref接收的是对象类型,.value为Proxy对象ref接收的是基本类型,.value为响应式数据
<template>
<div class="person">
<h2>汽车信息</h2>
<h2>一辆{{ car.brand }}车价值:{{ car.price }}w</h2>
<button @click="changePrice">修改汽车的价格</button>
<br />
<h2>游戏列表</h2>
<ul>
<li v-for="game of games" :key="game.id">{{ game.name }}</li>
</ul>
<button @click="changeFirstGame">修改第一个游戏的名字</button>
</div>
</template>
<script setup lang="ts" name="Person">
import { ref } from "vue";
// 数据
let car = ref({ brand: "奔驰", price: 100 });
let games = ref([
{ id: "dshj01", name: "原神" },
{ id: "dshj01", name: "和平精英" },
{ id: "dshj01", name: "王者荣耀" },
]);
console.log(games); // Ref
console.log(games.value); // Proxy
// 方法
function changePrice() {
// ref需要.value才能实现响应式修改
car.value.price += 10;
console.log(car.value.price);
}
function changeFirstGame() {
// 对象名.value.属性名 = 新值
games.value[0].name = "绝地求生";
}
</script>注意:
ref定义对象类型的响应式数据,在修改时需要使用对象名.value.属性名 = 新值因为ref对象的value属性才能获取到值,但在模板中使用时不要.value模板会自动添加
3.6 ref 对比 reactive
定义数据类型:
ref用来定义:基本类型数据,对象类型数据(any)reactive用来定义:对象类型数据(Object)
区别:
ref创建的变量必须使用.value(可以使用volar插件自动添加.value)
reactive定义的响应式对象不可整体被修改,否则会失去响应式(可以使用Object.assign合并)
let car = reactive({ brand: "奔驰", price: 100 });
function changeCar() {
car = { brand: "奥运", price: 20 }; // car失去响应式页面不更新
car = reactive({ brand: "奥运", price: 20 }); // 不是响应式
// 原因:car原本是一个Proxy对象,被赋值为一个普通对象,因此失去响应式
Object.assign(car, { brand: "奥运", price: 20 });
// 解决办法:对象合并不改变原对象
}使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref - 若需要一个响应式对象,层级不深,
ref、reactive都可以 - 若需要一个响应式对象,且层级较深,推荐使用
reactive - 若需要一个响应式对象,且需要频繁进行整体替换,推荐使用
ref
总结:定义响应式数据使用
ref即可,不必使用reactive
3.7 toRefs 与 toRef
作用:将一个
reactive响应式对象中的每一个属性,转换为ref对象使用场景:将一个
reactive定义的响应式对象解构赋值时,配合toRefs使用,解构出的变量均为ref响应式对象,且解构出来的响应式属性等同于源对象中的属性。(简化响应对象中属性的修改)(普通解构后,修改解构后的数据不会影响源数据)
<template>
<div class="person">
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
</div>
</template>
<script setup lang="ts" name="Person">
import { reactive, toRef, toRefs } from "vue";
// 数据
let person = reactive({
name: "张三",
age: 18,
});
// 通过toRefs将person对象中的n个属性批量取出,依然保持响应式的能力
console.log(toRefs(person)); // {name: ObjectRefImpl, age: ObjectRefImpl}
// 通过解构赋值,将toRefs生成的响应式属性赋值给变量
let { name, age } = toRefs(person);
console.log(name, age); // ref响应对象
let nl = toRef(person, "age");// 通过toRef将person对象的某个属性转换为响应式属性
console.log(nl, nl.value); // ref响应对象
// 方法
function changeName() {
// person.name += "~"; // 不使用toRefs
name.value += "~"; // 直接修改响应属性
console.log(name.value === person.name); // 在模板中使用name/person.name均可
}
function changeAge() {
age.value += 1;
}
</script>tips:
toRefs和toRef功能一致,但toRefs可以批量转换- 解构出来的响应式属性等同于对象中的属性
jslet { name, age } = toRefs(person); // person reactive相应式数据 console.log(name.value === person.name); // true 解构出来的属性为 ref响应式数据
toRefs转换的为ref响应式数据,需要使用.value修改响应数据
3.8 computed
作用:根据已有数据计算出新数据(和 Vue2 中的 computend 作用一致)
返回值:ref 响应式对象
何时调用:读取计算属性的值时(有缓存只调用一次),依赖计算的属性发生改变时
计算属性值会基于其响应式依赖被缓存,一个计算属性仅会在其响应式依赖更新时才重新计算。(可以进行性能优化,用于计算一些计算量较大且不需要每次重新渲染时进行计算)
两种写法:
- 只可读
let c1 = computend(()=>{
return ... // 返回计算属性的值
})- 可读可修改
let c2 = computend({
get(){ // 读取时调用
return ...// 返回计算属性的值
},
set(val){ // 修改时调用
...// 修改计算属性依赖计算的属性重新调用get函数
},
})计算属性实现姓名案例:
<template>
<div class="person">
姓:<input type="text" v-model.trim="firstName" /><br />
名:<input type="text" v-model.trim="lastName" />
<button @click="changeFullName">将全名改为 li-si</button><br />
<div>姓名:{{ fullName }}</div>
<div>姓名:{{ fullName }}</div>
<div>姓名:{{ fullName2() }}</div>
<div>姓名:{{ fullName2() }}</div>
</div>
</template>
<script setup lang="ts" name="Person">
import { computed, ref } from "vue";
let firstName = ref("zhang");
let lastName = ref("san");
// 方法实现
function fullName2() {
// 方法执行时没有缓存,调用一次就执行一次
console.log("方法执行了");
return (
firstName.value.charAt(0).toUpperCase() +
firstName.value.slice(1) +
"-" +
lastName.value
);
}
// 计算属性实现 (这样定义的计算属性只可读,不可修改)
let fullName = computed(() => {
// 计算属性存在缓存,只有当依赖计算的数据发生变化时才重新计算
console.log("computed执行了");
return (
firstName.value.charAt(0).toUpperCase() +
firstName.value.slice(1) +
"-" +
lastName.value
);
});
console.log(fullName); // ComputedRefImpl 计算属性返回值为一个响应式ref对象
// fullName.value = "newName"; // 无法为value赋值,因为它是只读的
// 计算属性实现(这样定义的计算属性是可读可写的)
let fullName = computed({
get() {
return (
firstName.value.charAt(0).toUpperCase() +
firstName.value.slice(1) +
"-" +
lastName.value
);
},
set(val) {
console.log(val); // val:计算属性被修改为的值
// 修改计算属性依赖计算的属性值,实现计算属性值的修改
[firstName.value, lastName.value] = val.split("-");
},
});
function changeFullName() {
fullName.value = "li-si"; // 修改计算属性的值时,调用computed中的set方法
}
</script>3.9 watch
作用:监视数据的变化(和 Vue2 中的 watch 作用一致)
特点:Vue3 中的 watch 只能监视以下四种数据:
ref定义的数据(包括计算属性)reactive定义的数据- 函数返回一个值(
getter函数)(常用) - 一个包含上述内容的数组
注意:不能直接监听响应式对象的属性值,需要使用一个返回该属性的
getter函数
在 Vue3 中使用 watch 的时候,通常会遇到如下几种情况:
3.9.1 情况一(常用):
监视 ref 定义的基本类型数据:直接写出数据名即可,监视的是其 value 值的改变
返回值:用于停止监视的函数
语法:
const stopWatch = wacth(属性名,(newValue,oldValue)={
....
stopWatch(); //停止监视
})实例:
<template>
<div class="person">
<h1>情况一:监视【ref】定义的【基本类型】数据</h1>
<h2>当前求和值为:{{ sum }}</h2>
<button @click="changeSum">点我sum加一</button>
</div>
</template>
<script setup lang="ts" name="Person">
import { ref, watch } from "vue";
// 定义数据
let sum = ref(0);
// 定义方法
function changeSum() {
sum.value += 1;
}
// 监视,情况一:监视【ref】定义的【基本类型】数据
const stopWatch = watch(sum, (newVlue, oldValue) => {
console.log("sum变化了", newVlue, oldValue);
if (newVlue >= 10) {
stopWatch(); // 停止监视
}
});
console.log(stopWatch);// watch返回一个函数,用于停止监视
</script>3.9.2 情况二:
监视 ref 定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】(整个对象),若想监视对象内部的数据,要手动开启深度监视 deep:true
语法:
const stopWatch = watch(
person, // 被监视对象
(newValue,oldValue) => {
.....
},
{ deep:true, immediate:true }
// 配置项:开启深度监视和初始化时执行一次
)watch的三个参数:
- 第一个参数:被监视的对象
- 第二个参数:监视的回调
- 第三个参数:配置对象
watch的返回值:停止监视的函数
实例:
<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 setup lang="ts" name="Person">
import { ref, watch } from "vue";
// 数据
let person = ref({
name: "张三",
age: 18,
});
// 方法
// 修改对象中的属性,需要开启深度监视且oldValue和newValue相同
function changeName() {
person.value.name += "~";
}
function changeAge() {
person.value.age += 1;
}
// 修改整个对象
function changePerson() {
person.value = { name: "李四", age: 20 };
}
// 监视,情况二:监视的是【ref】定义的【对象类型】数据,监视的是对象的地址值,
// 若想监视对象内部属性的变化,需要手动开启深度监视
watch(
person, // 被监视的对象
(newValue, oldValue) => {
console.log("peroson变化了", newValue, oldValue); // proxy
},
{ deep: true, immediate: true } // 开启深度监视,初始时调用一次
);
</script>注意:
- 若修改的是
ref定义的对象中的属性,newValue和oldValue都是新值,因为他们时同一个对象,指向同一个地址- 若修改整个
ref定义的对象,newValue是新值,oldValue是旧值,因为不是同一个对象了
3.9.3 情况三:
监视 reactive 定义的【对象类型】数据,且默认开启了深度监视,且监视回调函数中的 newValue 和 oldValue 相同
import { reactive, watch } from "vue";
// 数据
let person = reactive({
name: "张三",
age: 18,
});
// 方法
function changeName() {
person.name += "~";
}
function changeAge() {
person.age += 1;
}
// 修改整个对象
function changePerson() {
// person = { name: "李四", age: 20 };// reactive不可整体被修改
Object.assign(person, { name: "李四", age: 20 }); // 解决办法
}
// 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认开启深度监视
watch(person, (newValue, oldValue) => {
// newValue === oldValue 默认开启了深度监视
console.log("person变化了", newValue, oldValue);
});注意:
若使用
reactive定义对象类型数据,是不可以直接修改整个对象的(obj={..} 失去响应式 ),要使用Object.assign(obj,{...})来进行合并(地址不发生变化)
3.9.4 情况四(常用):
监视 ref 或 reactive 定义的【对象类型】数据中的某个属性 分以下两种情况:
- 若要监视的属性值是基本类型,需要写成函数式,函数返回要监视的属性
let person = reactive({
name: "张三",
age: 18,
car: {
c1: "奔驰",
c2: "宝马",
},
});
....
// 监视对象属性为基本类型使用函数式
watch(
() => person.name, //使用函数返回要监视的值
(newValue, oldValue) => {
console.log("person变化了", newValue, oldValue);
}
);- 若该属性值是对象类型,可直接写,也可以写成函数返回要监视的属性,建议写成函数式+
deep:true
let person = reactive({
name: "张三",
age: 18,
car: {
c1: "奔驰",
c2: "宝马",
},
});
....
// 监视的对象属性值为对象类型,最好使用函数式+deep:true
watch(
() => person.car, // 使用函数返回要监视的值
(newValue, oldValue) => {
console.log("person.car变化了", newValue, oldValue);
},
{ deep: true } // 开启深度监视
);结论:要监视对象中的某个属性,那么最好写函数式,如果对象的属性仍是对象类型,那么监视的是地址值,需要在内部加上
deep:true
Tips:函数式也适用于
ref定义的基本类型响应式数据,要加.value
3.9.5 情况五:
监视上述多个数据
将(ref定义的数据,reactive定义的数据,getter函数)放入一个数组内
// 数据
let person = reactive({
name: "张三",
age: 18,
car: {
c1: "奔驰",
c2: "宝马",
},
});
....
// watch,监视多个数据
watch(
[() => person.name, () => person.car.c1], // 要监视的多个数据
(newValue, oldValue) => {
// 监视回调
console.log("person.name 或 person.car.c1 发生变化", newValue, oldValue);
// ['张三~', '奔驰'] ['张三', '奔驰']
},
{ deep: true } // 深度监视
);tips:newValue为一个包含被监视内容的数组
3.10 watchEffect
官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
监视回调执行时机:最开始执行一次,监视函数中用到的数据改变时执行
watch 对比 watchEffect
- 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch:要明确指出监视的数据watchEffect:不用明确指出监视的数据(监视函数中用到哪些属性,就监视哪些属性)
示例:
<template>
<div class="person">
<h2>当水温达到60℃或水位达到80cm时,给服务器发请求</h2>
<h2>当前水温:{{ temp }}℃</h2>
<h2>当前水位:{{ height }}cm</h2>
<button @click="changeTemp">水温加10</button>
<button @click="changeHeight">水位加10</button>
</div>
</template>
<script setup lang="ts" name="Person">
import { ref, watch, watchEffect } from "vue";
// 数据
let temp = ref(10);
let height = ref(10);
// 方法
function changeTemp() {
temp.value += 10;
}
function changeHeight() {
height.value += 10;
}
// watch实现:需要指明监视的属性
watch([temp, height], ([newTemp, newHeigth]) => {
if (newTemp >= 60 || newHeigth >= 80) {
console.log("给服务器发请求");
}
});
// watchEffect实现,不需要指定监视的属性,不根据监视函数中的属性自动监视
watchEffect(() => {
if (temp.value >= 60 || height.value >= 80) {
console.log("给服务器发请求");
}
});
</script>3.11 标签的 ref 属性
作用:获取DOM或组件的实例对象
用在普通
html标签上,获取的是DOM节点
使用步骤:
为元素绑定
ref属性html<h2 ref="h2">河南</h2>创建变量用于存储
ref标记的内容(变量名要和ref标记名一致)jslet h2 = ref();获取
ref绑定元素的 DOM元素jsh2.value
<template>
<div class="person">
<!-- 1.为元素绑定ref属性 -->
<h2 ref="title2">河南</h2>
<button @click="showLog">点我输出h2元素</button>
</div>
</template>
<script setup lang="ts" name="Person">
import { ref } from "vue";
// 2.创建一个title2用于存储ref标记的内容(变量名要和ref标记一致)
let title2 = ref();
function showLog() {
// 3.获取ref标记元素的DOM元素
console.log(title2.value);
}
</script>用在组件标签上,获取的是组件实例对象
使用步骤:
为子组件绑定
ref属性html<Person ref="personRef" />获取子组件的实例对象,创建变量保存
ref标记的内容(要与ref标记名一致)jslet personRef = ref()在子组件中暴露数据供父组件中
ref获取jsdefineExpose({x,x,x})调用子组件暴露的属性/方法
jspersonRef.value.x
<template>
<!-- 1.为子组件绑定ref属性 -->
<Person ref="ren" />
<button @click="showLog">测试</button>
</template>
<script setup lang="ts" name="App">
import { ref } from "vue";
import Person from "./components/Person.vue";
// setup会自动将模板中的数据进行返回
// 2.创建变量保存ref标记内容
let ren = ref();
function showLog() {
console.log(ren.value);
// 在父组件中给子组件标签绑定ref,获取到的是子组件的实例对象, 只能访问到子组件通过defineExpose暴露出来的数据
}
</script>注意:
- 用于存储
ref标记内容的变量名必须与ref标记名一致- 为组件标签添加
ref标记,要想获取到组件实例对象上的数据,需要在子组件中使用defineExpose进行暴露
3.12 回顾接口、自定义类型,泛型
将TS的类型限制定义在 src/types/index.ts 文件
// 定义一个接口用于限制person对象的具体属性
export interface PersonInter {
id: string;
name: string;
age: number;
}
// 一个自定义类型限制一个数组内使用PersonInter的类型
export type Persons = Array<PersonInter>
export type Persons = PersonInter[]在vue文件中引入并使用
// 引入类型时,需要在加上 type
import type { PersonInter, Persons } from "@/types"; // @ = src
let person: PersonInter = { id: "hashfash2", name: "张三", age: 60 };
let personList: Persons = [
{ id: "hashfash2", name: "张三", age: 60 },
{ id: "hashfash3", name: "李四", age: 20 },
{ id: "hashfash4", name: "王五", age: 30 },
];在组件中引入接口、自定义类型.....时需要加上
type在路径中
@ === /src若一个文件下存在
index.js/ts引入时只需写到文件夹名,会自动寻找index.js/ts
3.13 props
作用:父组件给子组件传递数据(父子组件间通信)
实现步骤:
- 定义接口、自定义类型用于限制数据类型
// 定义一个接口,限制每一个Person对象的格式
export interface PersonInter{
id:string,
name:string,
age:number
}
// 自定义类型限制数组Persons
export type Persons = PersonInter[]- App.vue 为子组件传递数据
<template>
<!-- 使用props给子组件传递数据 -->
<Person a="哈哈" :list="personList" />
</template>
<script setup lang="ts" name="App">
import { reactive } from "vue";
import Person from "./components/Person.vue";
// 引入定义的类型
import { type Persons } from "@/types";
// reactive定义数据时使用泛型限制数据类型
let PersonList = reactive<Persons>([
{ id: "fdhgjhg01", name: "张三", age: 18 },
{ id: "fdhgjhg02", name: "李四", age: 20 },
{ id: "fdhgjhg03", name: "王五", age: 22 },
])
</script>- Person.vue 在子组件中接收数据
<template>
<!-- 渲染接收到的数据 -->
<div class="person">
<h2>{{ a }}</h2>
<ul>
<li v-for="item of list" :key="item.id">
{{ item.name }}---{{ item.age }}
</li>
</ul>
</div>
</template>
<script setup lang="ts" name="Person">
import { type Persons } from "@/types";
// 四种接收数据的方式:
//1.只接收数据
defineProps(["a","list"]);
//2.接收数据+类型限制(泛型)
defineProps<{ list: Persons; a: string }>();
//3.接收数据+类型限制(泛型)+限制必要性(?)+指定默认值
defineDefault(defineProps<{ list: Person; a?: string }>(),{ a:"我是默认值",list: ()=> [{ id: "131v", name: "张三", age: 18 }] });
// 4.接收数据并保存
let x = defineProps(["a","list"]); // Proxy(Object) {a: '哈哈', list: Proxy(Array)}
console.log(x.a); // 哈哈
x.a = "呵呵"; // 无法对a赋值,因为它是只读属性注意:
defineProps接收到的数据为响应式的可以直接在模板中使用,但不能直接在 js 代码中使用,需要进行接收props传递的数据只可读,不可修改reactive定义数据时使用泛型限制数据类型let fruits = reactive<TypeFruits>(),不可以直接let fruits: TypeFruits = reactive()
3.14 生命周期
概念:
Vue组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子规律:
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁、每个阶段都有两个钩子,一前一后
Vue2的生命周期(选项式API):
创建阶段:
beforeCreate、created挂载阶段:
beforeMount、mounted更新阶段:
beforeUpdate、updated销毁阶段:
beforeDestroy、destroyed
Vue3的生命周期(组合式API):
创建阶段:
setup(执行setup函数中的内容)挂载阶段:
onBeforeMount、onMounted(渲染组件)更新阶段:
onBeforeUpdate、onUpdated(更新组件)卸载阶段:
onBeforeUnmount、onUnmounted
- 常用的钩子:
onMounted(挂载完毕:发请求)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前:关闭定时器)
示例:
<script setup lang="ts" name="Person">
import {
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onMounted,
onUnmounted,
onUpdated,
ref,
} from "vue";
// 数据
let sum = ref(0);
// 方法
function add() {
sum.value += 1;
}
// 创建
console.log("创建");
// 挂载前: Vue3在挂载前调用onBeforeMount中指定的回调函数
onBeforeMount(() => {
console.log("挂载前");
});
// 挂载完毕:Vue3在挂载完毕调用onMounted中指定的回调函数
onMounted(() => {
console.log("子---挂载完毕");
});
// 更新前:Vue3在更新前调用onBeforeUpdate中指定的回调函数
onBeforeUpdate(() => {
console.log("更新前");
// debugger;
});
// 更新完毕:Vue3在更新完毕调用onUpdated中指定的回调函数
onUpdated(() => {
console.log("更新完毕");
});
// 卸载前:Vue3在卸载前调用onBeforeUnmount中指定的回调函数
onBeforeUnmount(() => {
console.log("卸载前");
});
// 卸载完毕:Vue3在卸载完毕用onUnmount中指定的回调函数
onUnmounted(() => {
console.log("卸载完毕");
});
</script>3.15 自定义hook
什么是
hook?本质是一个函数,把
setup函数中使用的Compositions API进行了封装,类似于Vue2.x中的mixin自定义
hook的优势:代码复用,让
setup中的逻辑更加清楚易懂作用:
将一个组件中不同功能的数据、方法、计算属性..... 分离成独立的
hook,使不同功能代码维护起来更加方便,组件中的代码也更加清晰,也便于代码的复用
使用示例:
- 在
src/hooks下创建usexxx.ts文件,将功能代码放在一个函数中,将要暴露的数据使用return返回,最后将函数暴露。
scr/hooks/useSum.ts(求和相关功能):
import { computed, onMounted, ref } from "vue";
// 将函数暴露
export default function () {
// 数据
let sum = ref(0);
// 计算属性
let bigSum = computed(() => {
return sum.value * 10;
});
// 方法
function add() {
sum.value += 1;
}
// 钩子
onMounted(() => {
sum.value += 100;
});
// 向外部提供数据
return { sum, bigSum, add };
}useDog.ts(获取狗图片相关功能):
import { onMounted, reactive } from "vue";
import axios from "axios";
// 将数据和方法放在一个函数中,然后将函数暴露
export default function () {
// 数据
let dogList = reactive<string[]>([]);
// 方法
// 使用async和await代替then,(try catch)/拦截器代替catch
async function getDog() {
try {
// result 接收请求成功返回的数据
let result = await axios.get(
"https://dog.ceo/api/breed/pembroke/images/random"
);
dogList.push(result.data.message); // 成功处理
} catch (error) {
// 请求失败 error:错误信息
alert(error); // 失败处理
}
}
// 钩子
onMounted(() => {
getDog();
});
// 向外界提供的属性、方法
return { dogList, getDog };
}- 在组件中引入
hook并调用hook函数获取到数据
<template>
<div class="person">
<h2>当前求和为:{{ sum }}</h2>
<h2>求和放大10倍:{{ bigSum }}</h2>
<button @click="add">点我加一</button>
<hr />
<img v-for="(item, index) of dogList" :key="index" :src="" data-missing="item" /><br />
<button @click="getDog">再来一只修狗</button>
</div>
</template>
<script setup lang="ts" name="Person">
// 引入hooks
import useSum from "@/hooks/useSum";
import useDog from "@/hooks/useDog";
// 调用hook函数获取数据
const { sum, bigSum, add } = useSum();
const { dogList, getDog } = useDog();
</script>注意:
scr/hooks下的文件名必须为usexxx.ts- 在每个
hook中可以写钩子函数,计算属性....hook函数为匿名函数,且使用默认暴露,将要暴露的数据通过return返回
3.16 axios 配合 async+await
async await配合使用可以代替 then回调函数,使代码变得更加同步,但不能进行错误处理,可以使用 try catch包裹或使用请求拦截器进行处理
// 使用async和await代替then,(try catch)/拦截器代替catch
async function getDog() {
try {
// result 接收请求成功返回的数据 await会阻塞后续代码执行
let result = await axios.get(
"https://dog.ceo/api/breed/pembroke/images/random"
);
// 返回成功的Promise后才会执行下面语句
dogList.push(result.data.message); // 成功处理
} catch (error) {
// 请求失败 error:错误信息
alert(error); // 失败处理
}
}四、路由
4.1 对路由的理解
实现 SPA (单页面应用),前端路由(route)就是一组key(路径),value(组件)对应关系,多个路由,需要经过一个路由器(router)的管理

4.2 路由基本使用
安装
vue-router:npm i vue-router创建
router/index.ts文件夹配置路由器
// 1.引入createRouter
import { createRouter, createWebHistory } from "vue-router";
// 引入路由路由组件
import About from "@/components/About.vue";
import Home from "@/components/Home.vue";
import News from "@/components/News.vue";
// 2.创建路由器
const router = createRouter({
// 设置路由器的工作模式
history: createWebHistory(),
routes: [ // 路由规则
{
path: "/home",
component: Home,
},
{
path: "/about",
component: About,
},
{
path: "/news",
component: News,
},
]
})
// 3.将router暴露
export default router;- 创建路由组件
src/views - 在
main.ts中引入并使用路由器
...
// 1.引入路由器
import router from "./router";
// 创建应用
const app = createApp(app);
// 2.使用路由器
app.use(router);
// 挂载
app.mount("#app");- 在导航组件中设置
<RouterLink to='xx'></RouterLink>和<RouterView></RouterView>标签,用于切换路由和展示路由组件
<template>
<div class="app">
<h2 class="title">Vue3路由测试</h2>
<!-- 导航区 -->
<div class="navigate">
<!-- active-class='xx' 指定组件激活时的样式 -->
<RouterLink to="/home" active-class="active">首页</RouterLink>
<RouterLink to="/news" active-class="active">新闻</RouterLink>
<RouterLink to="/about" active-class="active">关于</RouterLink>
</div>
<!-- 展示区 -->
<div class="main-content">
<!-- 路由组件展示的位置 -->
<RouterView></RouterView>
</div>
</div>
</template>tips:
RouterLink标签的active-class="xxx"属性可以指定组组件被激活时的样式类名
4.3 两个注意点
- 路由组件通常放在
view或pages文件夹,公共组件通常放在components文件夹 - 路由组件的切换实际是进行了卸载和重新挂载的过程,如果需要缓存路由组件可以使用
<keep-alive><keep-alive>组件 keepAlive
4.4 路由器的工作模式
history模式(常用)React:BrowserRouterVue2:mode: 'history'Vue3:history: createWebHistory()优点:
URL更加美观不带有#,更接近传统网站URL缺点:后期项目上线,需要服务器端配合处理路径问题,否则刷新会有
404错误hash模式Vue2:mode: 'hash'Vue3:history: createWebHashHistory()优点:兼容性更好,且服务器端不需要处理路径,
#后面的路径不会带给服务器缺点:
URL带有#不美观,且在SEO优化方面相对较差
4.5 to的两种写法
- 字符串写法
<RouterLink to="/news" active-class="active">新闻</RouterLink>- 对象写法(常用)
<RouterLink :to="{path:'/about',name:"",params:"",query:""}" active-class="active">关于</RouterLink>可以在对象中添加 name、query、params....属性
4.6 命名路由
使用:为每个路由规则添加一个 name 属性
作用:可以简化路由跳转及传参
使用 to 的对象写法配合 name 属性进行路由跳转
// 路由规则
{
name: "guanyu", // 命名路由
path: "/about", // 可以替代写路径
component: About,
},
// App.vue
<RouterLink :to="{ name: 'guanyu' }" active-class="active">关于</RouterLink>4.7 嵌套路由
在一个路由规则中嵌套子路由规则,使用 children 配置项
作用:用于子路由组件中的导航
编写 News 的子路由:Detail.vue
{
name: "xinwen",
path: "/news",
component: News,
// 嵌套路由使用children配置项
children: [
{
name: "xiangqing", // 命名路由
path: "detail", // 写法一
path: "/news/detail" // 写法二
component: Detail,
},
],
},在子路由组件中使用:
<template>
<div class="news">
<!-- 导航区 -->
<ul>
<li v-for="item of newsList" :key="item.id">
<RouterLink :to="{ name: 'xiangqing' }" active-class="active">{{
item.title
}}</RouterLink>
</li>
</ul>
<!-- 展示区 -->
<div class="news-content">
<RouterView></RouterView>
</div>
</div>
</template>
<script setup lang="ts" name="News">
import { reactive } from "vue";
const newsList = reactive([
{ id: "ndks01", title: "抗癌食物", content: "西瓜" },
{ id: "ndks01", title: "如何一夜暴富", content: "学IT" },
{ id: "ndks01", title: "震惊,万万没想到", content: "明天是周一" },
{ id: "ndks01", title: "好消息", content: "快放假了" },
]);
</script>注意:
- 配置子路由的
path有两种写法
- 写路径全称
/parent/children- 只写子路由路径
children(注意:不要写/)- 在
router-link标签的to属性中路径要写完整- 如果嵌套的层级较多,可以使用
to的对象写法,配合name属性
4.8 路由传参
4.8.1 query参数
形式:http://localhost:5173/news/detail?id=123title=haha
传递参数(两种方法)
to的字符串写法
vue<li v-for="item of newsList" :key="item.id"> <RouterLink :to="`/news/detail?id=${item.id}&title=${item.title}&content=${item.content}`" >{{ item.title }}</RouterLink > </li>to的对象写法(推荐)
vue<li v-for="item of newsList" :key="item.id"> <RouterLink :to="{ name: 'xiangqing', query: { id: item.id, title: item.title, content: item.content }, }" >{{ item.title }}</RouterLink> </li>接收参数并使用
vue<template> <ul class="news-list"> <li>编号:{{ query.id }}</li> <li>标题:{{ query.title }}</li> <li>内容:{{ query.content }}</li> </ul> </template> <script setup lang="ts" name="About"> import { toRefs } from "vue"; import { useRoute } from "vue-router"; // 接受query参数,usePouter为一个hook const route = useRoute(); console.log(route); // route为proxy响应式对象内部有当前路由的全部信息,且当route发生变化时会重新解析页面 // 解构获取query参数 const { query } = toRefs(route); // toRefs接收一个reactive定义的响应式对象 // 注意:直接解构route获取到的属性不是响应式的,配合torefs获取响应式属性 </script>注意:
route中保存着当前路由的全部信息,需要使用useRoute()去获取- 直接解构一个
reactive响应式对象,获取到的属性会失去响应式,需要配合toRefs进行解构 toRefs接收的是一个reactive的响应式对象
4.8.2 params参数
形式:http://localhost:5173/news/detail/params1/params2
传递参数(两种方式):
to的字符串写法
vue<li v-for="item of newsList" :key="item.id"> <RouterLink :to="`/news/detail/${item.id}/${item.title}/${item.content}`" >{{ item.title }}</RouterLink > </li>to的对象写法
vue<li v-for="item of newsList" :key="item.id"> <RouterLink :to="{ name: 'xiangqing', params: { id: item.id, title: item.title, content: item.content, }, }" >{{ item.title }}</RouterLink> </li>注意:使用
to的对象写法传递params参数时,必须使用name属性,而不能使用path属性声明接收:
在路由规则中声明接收的参数
js{ name: "xinwen", path: "/news", component: News, // 嵌套路由使用children配置项 children: [ { name: "xiangqing", // path: "detail", path: "detail/:id/:title/:content?", // 声明接收params参数 ?表示可选参数 component: Detail, }, ], },注意:传递
params参数时,需要提前在路由规则中占位,/:aa??表示该参数是可选的,可以不传递接收参数并使用
vue<template> <ul class="news-list"> <li>编号:{{ params.id }}</li> <li>标题:{{ params.title }}</li> <li>内容:{{ params.content }}</li> </ul> </template> <script setup lang="ts" name="About"> import { useRoute } from "vue-router"; import { toRefs } from "vue"; // 获取路由对象 const route = useRoute(); // 获取params参数并转换为响应式 const { params } = toRefs(route); </script>
4.9 路由的props配置
作用:让路由组件更加方便到的收到参数(可以将路由参数作为 props 传给组件)
使用:在路由配置中添加 props 配置项,通过 props 配置项将参数传递给路由组件(路由组件不使用组件标签,因此不能直接以 <Person a='xx' :b='xx'> 的形式传递props)
路由的props 配置的三种写法:
props的布尔值写法:将路由收到的所有
params参数以props形式传递给路由组件ts{ name: "xiangqing", path: "detail", // path: "detail/:id/:title/:content?", component: Detail, .... props: true, }只适用于
params参数props的函数写法:可以接收到
route对象作为参数,return一个对象作为props传递给路由组件(可以传递params,query参数)ts{ name: "xiangqing", path: "detail", // path: "detail/:id/:title/:content?", component: Detail, ..... props(route) { return { id: route.query.id, title: route.query.title, content: route.query.content, }; }, }通用写法:可以传递
params和query参数对象写法:
自定义
props传递的数据,(为固定数据)ts{ name: "xiangqing", path: "detail", // path: "detail/:id/:title/:content?", component: Detail, ..... props: { id: "0001", title: "标题1", content: "内容1", }, }由于数据是固定的,在开发中一般不使用
路由组件接收使用路由props 配置传递的数据
<template>
<ul class="news-list">
<li>编号:{{ id }}</li>
<li>标题:{{ title }}</li>
<li>内容:{{ content }}</li>
</ul>
</template>
<script setup lang="ts" name="About">
// 使用 props 接受参数
// defineProps(["id", "title", "content"]);
// 使用泛型限制接收到参数的类型
defineProps<{ id: string; title: string; content: string }>();
</script>4.10 replace属性
作用:控制路由跳转时操作浏览器历史记录的模式
浏览器的历史记录有两种写入方式:分别为
push和replace:push是追加历史记录(默认值)replace是替换当前记录
开启
replace模式:vue<RouterLink relpace ...>News</RouterLink>
4.11 编程式路由导航
作用:脱离
<RouterLink>实现路由跳转使用:
Vue2中使用:$route和$router获取路由对象、和路由器对象Vue3中使用:useRoute()和useRouter()两个hooks获取路由对象、和路由器对象API:
import { useRouter } from "vue-router";
// 获取路由器对象
const router = useRouter(); //useRouter()为一个hooks
// 1.使用push模式路由跳转
router.push(options) ;
// 2.使用replace模式路由跳转
router.replace(options);
// 3.前进
router.forward();
// 4.后退
router.back();
// 5.前进/后退指定步
router.go(n)注意:
options与RouterLink标签中的to相同(可以为字符串/对象)
编程式路由导航使用场景:
符合某些条件进行路由跳转(登录成功)
在一些特定标签、特定事件回调中进行路由跳转
示例:
<template>
<ul>
<!-- 传递query参数 -->
<li v-for="item of newsList" :key="item.id">
<button @click="showNewsDetail(item)">查看新闻</button>
......
</li>
</ul>
<!-- 展示区 -->
<div class="news-content">
<RouterView></RouterView>
</div>
</template>
<script>
...
// 获取路由器实例
const router = useRouter();
// 定义一个接口限制参数类型
interface NewsInter {
id: string;
title: string;
content: string;
}
// 点击按钮查看新闻
function showNewsDetail(item: NewsInter) {
router.push({
// punsh 内的配置对象与 RouterLink中的to相同(字符串/对象)
name: "xiangqing",
query: { id: item.id, title: item.title, content: item.content },
});
}
</script>4.12 路由重定向
- 作用:将特定的路径、重新定向到已有路径
- 使用:在路由配置中添加
redirect:'path'
....
{
path: "/",
redirect: "/home",
},注意:
redirect的值只能为path不能为name
4.13 路由守卫
4.13.1 全局前置路由守卫
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。
使用 router.beforeEach 注册一个全局前置守卫(一般用于路由鉴权,登录判断使用)
router.beforeEach((to, from, next) => {
.....
// 返回 false 以取消导航
return false
// 将用户重定向到登录页面
return { name: 'Login' }
})- to:要前往的路由对象
- from:当前路由对象
- next:放行函数
- return:可以返回
false: 取消当前的导航,一个路由地址
4.13.2 全局后置路由守卫
路由成功跳转后执行的钩子,可以用于改变页面标题
router.afterEach((to) => {
// 设置页面标题 访问某一路由后会执行该钩子
document.title = (setting.title.slice(0, 4) + '-' + to.meta.title) as string
nprogress.done() //关闭进度条
})4.14 路由缓存 + 两个全新的生命周期钩子
4.14.1 路由缓存
默认路由切换后被卸载,使用 <KeepAlive使组件不被卸载
<router-view v-slot="{ Component }"> // 作用域插槽
<keep-alive>
<component :is="Component" /> // 动态组件
</keep-alive>
</router-view>4.14.2 两个路由组件的生命周期钩子
- onActivated:被缓存的路由组件被激活时
- onDeactivated:被缓存的路由组件失活时
4.15 滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。
const router = createRouter({
history: createWebHashHistory(),
routes: [...],
// 滚动行为(页面跳转时回到顶部)
scrollBehavior: () => {
return {
top: 0,
left: 0
}
}
})五、pinia
作用:用于集中式状态(数据)管理
Vue3中使用的是piniaVue2中使用的是VuexReact中使用的是redux
使用场景:多组件共享数据时
5.1 准备一个效果
[Missing Image: pinia_example.gif]
5.2 搭建pinia环境
第一步:安装 pinia
npm i pinia
第二步:在 src/main.ts 中安装 pinia
import { createApp } from "vue";
import App from "./App.vue";
// 1.引入pinia
import { createPinia } from 'pinia';
const app = createApp(app);
//2.创建 pinia
const pinia = createPinia();
//3.安装 pinia
app.use(pinia);
app.mount("#app");5.3 存储+读取数据
store是一个保存:状态、业务逻辑 的实体,每个组件都可以 读取、写入它- 它有三个概念:
state、getter、action、相当于组件中的:data,computed和methods
3.5.1 存储数据
将要存储的数据存放在 store 文件夹中,文件名与组件名保持一致。
- 将
count.vue中共享的数据存储到store/count.ts中
// 1.引入 defineStore 用于创建 store
import { defineStore } from "pinia";
// 2.创建一个store用于存储count组件中的状态(数据)并暴露
export const useCountStore = defineStore("store",{
//3.储数据 state为一个函数,返回一个对象,对象内为要存储的数据
stata(){
return {
sum: 6; // 为响应式数据
}
}
})注意:
创建 store 时,命名使用 hooks 的命名方式
usexxxStore
defineStore(id,options)接收两个参数 id:store的唯一标识,options:配置项/setup函数
- 将
LoveTalk.vue中共享的数据存储到LoveTalk.ts中
import { defineStore } from "pinia";
export const useTalkStore = defineStore("talk", {
// 存储数据
state() {
return {
talkList: [
{
id: "tw001",
title: "今天你有点怪,哪里怪?怪好看的!",
},
],
};
},
});3.5.2 在组件中读取数据并使用
<template>
<!-- 使用 -->
<h2>当前求和为:{{ countStore.sum }}</h2>
</template>
<script>
// 获取
// 1.引入useCountStore
import { useCountStore } from "@/store/count";
// 2.调用useXxxxxStore得到对应的store
const countStore = useCountStore();// 返回值为一个 reactive 创建的响应式对象
// 3.获取state中的数据 方法一(常用)
console.log(countStore.sum); // sum为一个ref响应式对象
console.log(countStore.$state.sum) //方法二
</script>Tips:访问
reactive创建的响应式对象中ref创建的响应式数据时不需要加.value
5.4 修改数据(三种方式)
第一种方式:在组件中直接修改(适用于逻辑简单的情况)
tscountStore.sum += n.value;在组件中通过
countStore直接修改数据,store中的数据和页面都会发生变化第二种方式:在组件中批量修改
tscountStore.$patch({ school: "商丘学院", address: "开封市", })第三种修改方式:借助
action修改(action中可以编写一些业务逻辑)在组件中调用
actions中的方法tscountStore.increment(n.value);store/count.ts:tsexport const useCountStore = defineStore("count", { // actions中放置的方法用于响应组件中的动作 actions: { increment(value: number) { if (this.sum < 10) { this.sum += value; // this为当前store } }, }, state() { return { sum: 6, school: "sqxy", address: "商丘", }; }, });在
actions配置项内的函数的this指向当前store对象
当业务逻辑较为复杂时将逻辑性代码写在
actions中可以简化让组件更加清晰,也提高了代码的复用
5.5 storeToRefs
store是一个用reactive包装的对象,因此不能直接进行解构获取其state但可以直接解构action
store实例对象是一个reactive响应式对象,因此不能完全替换掉 store 的 state
借助 storeToRefs 将 store 中的数据解构为 ref 响应式对象,方便在模板中使用
<template>
<h2>当前求和为:{{ sum }}</h2>
<h3>欢迎带到 {{ school }},地址 {{ address }}</h3>
...
</template>
<script>
import { useCountStore } from "@/store/count";
import { storeToRefs } from "pinia";
// 得到CountStore
const countStore = useCountStore();
// 进行解构并转化为ref响应式对象
const {sum, school, address} = storeToRefs(countStore);
// 直接进行解构会失去响应式,对数据修改就会影响到仓库中的数据
.....
</script>注意:不要使用
toRefs进行转换
pinia提供的storeToRefs只会将state/getter中的数据转换,而Vue提供的toRefs会将store中的全部属性和方法转换为ref对象
Tips:可以直接解构获取
store内actions中的方法ts// 解构获取store中actions中的方法 const { increment } = countStore;
5.6 getters
作用:当
state中的数据,需要经过处理后再使用时,可以通过getters配置,相当于计算属性用法与计算属性也类似使用:追加
getters配置tsexport const useCountStore = defineStore("count", { actions: {...}, state() {...}, getters: { // 写法一:使用state参数 bigSum(state): number {// 可以接收一个参数state:当前store对象 return state.sum * 10; }, // 写法二:使用this upperSchool(): string { return this.school.toUpperCase();// this为当前store对象 }, }, });获取
在组件中直接通过
storeToRefs解构countStore进行获取 或countStore.xxx获取tsconst { sum, bigSum, school, address } = storeToRefs(countStore);
5.7 $subscribe
通过 store 的 $subscribe() 方法监听 state 的变化
// 监听state改变
talkStore.$subscribe((mutate, state) => { // state:变化后的state
console.log("talkStore中保存的数据发生了变化", mutate, state);
// 当store中的数据发生变化时将数据存储到localStorage中
localStorage.setItem("talkList", JSON.stringify(state.talkList));
});5.8 store组合式写法(推荐)
store 选项式写法:defineStore的第二个参数为一个配置对象
export const useTalkStore = defineStore("talk", {
// 方法
actions: {
async getATalk() {.....},
}
// 数据
state() {
return {
talkList: ....
};
},
// getters
getters:{
bingSum(){
return ...
}
}
});store 组合式写法:defineStore的第二个参数为一个 setup 函数
export const useTalkStore = defineStore("talk", () => {
// state:使用ref/reactive 定义的响应式数据
const talkList = ref(....)
// actions:function定义的方法
async function getATalk() {....}
// getters:computed定义的计算属性
const bigSum = computed(() => {
return sum.value * 10;
});
// 暴露数据
return { talkList, getATalk };
});推荐使用组合式,无需使用this就可以直接在函数中访问到数据,且没有太多层级
注意:使用组合式时必须对数据和方法进行
return暴露。
拓展 持久化存储pinia中的数据
持久化存储pinia中的数据的方案:
- 使用
localStroge.setItem()..... - 使用插件:pinia-plugin-persistedstate
pinia-plugin-persistedstate使用步骤:
安装:
pnpm i pinia-plugin-persistedstate在pinia 中使用插件
tsimport { createPinia } from 'pinia' + import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() + pinia.use(piniaPluginPersistedstate)自动存储 store 中的数据
创建 Store 时,将
persist选项设置为true。tsimport { defineStore } from 'pinia' import { ref } from 'vue' export const useStore = defineStore( 'main', () => { const someState = ref('你好 pinia') return { someState } }, + { + persist: true, + }, )存储数据,清除数据
当设置仓库中属性值时,会自动将该属性存储到浏览器本地存储中
存储格式: store名 {"属性名":"值"}当将属性值修改为
undefined时,会将该属性值从浏览器本地存储中清除
六、组件通信
6.1 vue3组件通信与 Vue2的区别:
- 移除了全局事件总线
$bus,使用mitt代替 vuex换成了pinia- 把
.sync优化到了v-model中 - 把
$listeners所有的东西,合并到了$attrs中 $children被砍掉了
常见搭配形式:

6.1 props
作用:props 是使用频率最高的一种通信方式,常用于:父组件 ↔ 子组件
父传子:属性值是 非函数
父组件通过
props将数据直接传递给子组件,子组件通过definProps进行接收即可在模板中使用子传父:属性值是 函数
父组件给子组件通过
props传递一个函数,子组件在合适的时候调用,通过参数的形式将数据传递给父组件
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<h4>汽车:{{ car }}</h4>
<h4 v-show="toy">玩具: {{ toy }}</h4>
<!-- 父组件向子组件传递数据和方法(使用v-bind) -->
<Child :car="car" :sendToy="getToy" />
</div>
</template>
<script setup lang="ts" name="Father">
import { ref } from "vue";
import Child from "./Child.vue";
// 数据
let car = ref("奔驰");
let toy = ref("");
// 方法
function getToy(value: string) {
console.log("父组件收到子组件的值:", value);
toy.value = value;
}
</script>子组件:
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<!-- 将父组件传递的数据进行展示 -->
<h4>父给的车:{{ car }}</h4>
<!-- 子组件调用父组件中的方法将数据传递给父组件 -->
<button @click="sendToy(toy)">把玩具给父亲</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
// 数据
let toy = ref("奥特曼");
// 声明接收props
const props = defineProps(["car", "sendToy"]);
</script>注意:在
defineProps中接收到的数据只能在模板中使用,若要在JS代码中使用需要进行接收
单向数据流:(不可进行修改)
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着不应该在子组件中去更改一个 prop。
6.2 自定义事件
作用:自定义事件常用于:子组件 => 父组件 间的通信
原生事件与自定义事件的区别:
- 原生事件:
- 事件名是特定的(
click、mosueenter等等) - 事件对象
$event: 是包含事件相关信息的对象(pageX、pageY、target、keyCode)
- 事件名是特定的(
- 自定义事件:
- 事件名是任意名称(推荐使用
kebab-case命名) - 事件对象
$event: 是调用emit时所提供的数据,可以是任意类型!!!
- 事件名是任意名称(推荐使用
示例:
父组件:为子组件标签绑定自定义事件,并将回调留在父组件中,子组件在合适的时候触发自定义事件。
<template>
<div class="father">
<h3>父组件</h3>
<h4 v-show="toy">子组件给我的玩具 {{ toy }}</h4>
<!-- 为子组件Child绑定多个自定义事件 -->
<Child @send-toy="getToy" @test="test" />
</div>
</template>
<script setup lang="ts" name="Father">
import { ref } from "vue";
import Child from "./Child.vue";
let toy = ref("");
// 将事件回调留在父组件中(用于获取数据)
function getToy(value: string) {
toy.value = value;
}
function test(){
console.log('test')
}
</script>子组件:使用 defineEmits 进行声明自定义事件,并在合适的时候 使用 emit 进行调用
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<!-- 在模板中使用 emit触发自定义事件,并传递数据-->
<button @click="emit('send-toy', toy)">给父组件玩具</button>
<button @click="emit('test')">test</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { onMounted, ref } from "vue";
// 数据
let toy = ref("奥特曼");
// 声明自定义事件,可以在任意时刻进行调用
const emit = defineEmits(["send-toy", "test"]); // 可以声明接收多个自定义事件
// 使用emit或$emit 固定写法
// 组件挂载三秒后给父组件玩具
onMounted(() => {
setTimeout(() => {
emit("sendToy", toy.value); // 触发自定义事件,并传递数据
}, 3000);
});
</script>Tips:自定义事件名推荐使用
kebab-case命名注意:使用
emit或$emit存储自定义事件,固定写法
6.3 mitt
描述:与全局事件总线 $bus 和消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信
使用:
安装
mitt:npm i mitt新建文件:
src\utils\emitter.ts创建并暴露
emitterts// 引入mitt import mitt from "mitt"; // 调用mitt()得到一个emitter对象并暴露:可以进行绑定、触发、解绑事件 const emitter = mitt() export default emitter
emitter 身上的 API:
emitter.on("事件名", 回调);绑定事件emitter.emit("事件名", [数据]);触发事件并传递参数emitter.off("事件名");解绑事件emitter.all.clear();获取全部绑定事件并解绑
在需要获取数据的组件中引入
emitter并绑定事件,并在组件被卸载时解绑事件vue<template> <div class="child2"> .... <h4 v-show="toy">收到了哥哥的玩具:{{ toy }}</h4> </div> </template> <script setup lang="ts" name="Child2"> // 引入emitter import emitter from "@/utils/emitter"; import { onUnmounted, ref } from "vue"; // 数据 let computer = ref("XXX电脑"); let toy = ref(""); // 给emitter绑定send-toy事件 onMounted(()=>{ emitter.on("send-toy", (value: any) => { toy.value = value; // 将获取到的数据进行保存 }); }) // 当组件被卸载时,解绑在emitter上绑定的事件 onUnmounted(() => { emitter.off("send-toy"); }); </script>提供数据的组件,在合适的时候触发事件
vue<template> <div class="child1"> ... <!-- 通过触发emitter中绑定的事件将数据传递给绑定事件的组件 --> <button @click="emitter.emit('send-toy', toy)">玩具给弟弟</button> </div> </template> <script setup lang="ts" name="Child1"> // 引入emitter import emitter from "@/utils/emitter"; import { ref } from "vue"; let toy = ref("奥特曼"); </script>
注意:
- 在组件被卸载时
onUnmounted钩子内,将绑定的事件进行解绑- 哪个组件需要数据就在该组件中绑定事件,在提供数据的组件中触发事件
6.4 v-model
描述:实现 父↔子 组件之间相互通信
v-model用在input标签上,实现数据双向绑定vue<input type="text" v-model="username" /> < v-model 底层原理 动态数据绑定+input事件> <input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value" />v-model用在组件标签上,实现父↔子组件通信父组件:
vue<templete> <AtguiguInput v-model="username" /> <!-- v-model 底层原理 --> <AtguiguInput :modelValue="username" // props传递数据 @update:modelValue="username = $event" // 自定义方法修改username的值 /> </templete> <script setup lang="ts" name="Father"> import { ref } from "vue"; import AtguiguInput from "./AtguiguInput.vue"; // 数据 let username = ref("张三"); </script>子组件(UI组件):
vue<template> <input type="text" :value="modelValue" @input="emit('update:modelValue',($event.target as HTMLInputElement).value)" /> </template> <script setup lang="ts" name="AtguiguInput"> // 父组件在子组件标签上添加v-model 子组件就可以接收到modelValue属性和update:modelValue事件 defineProps(["modelValue"]); // 获取父组件传递的数据 const emit = defineEmits(["update:modelValue"]) // 获取自定义事件,实现向父组件传递数据 </script>更换
modelValue并为组件绑定多个v-model父组件:
vue<!-- 修改modelValue --> <AtguiguInput v-model:name="username" v-model:psd="password"/>子组件(UI 组件):
vue<template> <input type="text" :value="name" @input="emit('update:name', ($event.target as HTMLInputElement).value)" /> <input type="password" :value="psd" @input="emit('update:psd', (<HTMLInputElement>$event.target).value)" /> </template> <script setup lang="ts" name="AtguiguInput"> // 获取父组件使用v-model传递的数据和自定义事件 defineProps(["name", "psd"]); // 获取父组件传递的数据 const emit = defineEmits(["update:name", "update:psd"]); // 获取自定义事件,实现向父组件传递数据 </script>
注意:
$event到底是什么?啥时候能用.target
- 对于原生事件,
$event就是事件对象,可以使用.target获取触发事件的DOM元素- 对于自定义事件,
$event就是触发事件时,所传递的数据(参数),不可以使用.targetTips:
v-model绑定的组件标签一般为 UI 组件标签
6.5 $attrs
- 描述:
$attrs用于实现当前组件的父组件 ,向当前组件的子组件通信(祖→孙) - 具体说明:
$attrs是一个对象,包含未被当前组件接收的父组件传递的数据
注意:
$attrs会自动排除当前组件使用defineProrps接收的属性Tips:
v-bind的对象写法v-bind="{a:123,b:456}"===:a="123" :b="456"
使用:
父组件:
vue<template> <div class="father"> <h3>父组件</h3> <h4>a:{{ a }}</h4> <h4>b:{{ b }}</h4> <h4>c:{{ c }}</h4> <h4>d:{{ d }}</h4> <!-- v-bind="{ x: 100, y: 200 }" === :x="100" :y="200" --> <!-- 将数据传递给子组件 --> <Child :a="a" :b="b" :c="c" :d="d" v-bind="{ x: 100, y: 200 }" :updateA="updateA" /> </div> </template> <script setup lang="ts" name="Father"> import { ref } from "vue"; import Child from "./Child.vue"; let a = ref(1); let b = ref(2); let c = ref(3); let d = ref(4); // 实现孙组件和父组件通信 function updateA(value: number) { a.value += value; } </script>子组件:
vue<template> <div class="child"> <h3>子组件</h3> <!-- 父组件使用props传递给子组件的属性,子组件未接收的就被存储在$attrs中 --> <!-- <h4>{{ $attrs }}</h4> --> <!-- 将子组件未接收的数据传递给孙组件 --> <GrandChild v-bind="$attrs"/> </div> </template>孙组件:
vue<template> <div class="grand-child"> <h3>孙组件</h3> <h4>a:{{ a }}</h4> <h4>b:{{ b }}</h4> <h4>c:{{ c }}</h4> <h4>d:{{ d }}</h4> <h4>x:{{ x }}</h4> <h4>y:{{ y }}</h4> <button @click="updateA(1)">点我将爷爷组件的a值加1</button> </div> </template> <script setup lang="ts" name="GrandChild"> // 接收父组件使用 v-bind="$attrs" 传递的的数据 defineProps(["a", "b", "c", "d", "x", "y", "updateA"]); </script>本质:父组件通过
props传递给子组件数据,子组件再通过v-bind="$attrs"将未接收的数据通过props传递给孙组件
6.6 $refs $parent
描述:
$refs用于:父→子 (通过$refs获取到全部使用ref属性绑定的子组件实例对象)$parent用于:子→父(通过$parent获取当前组件的父组件的实例对象)原理:
属性 说明 $refs值为对象,包含所有被 ref属性标识的DOM元素或组件实例对象$parent值为对象,当前组件的父组件的实例对象 回顾
ref在组件上使用vue<template> <div class="father"> <h3>父组件</h3> <button @click="changeToy">点我修改Child1的玩具</button> <button @click="changeComputer">点我修改Child2的电脑</button> <!-- 为子组件添加ref属性 --> <Child1 ref="c1" /> <Child2 ref="c2" /> </div> </template> <script setup lang="ts" name="Father"> import { ref } from "vue"; import Child1 from "./Child1.vue"; import Child2 from "./Child2.vue"; // 获取子组件的实例对象 let c1 = ref(); let c2 = ref(); // 修改子组件中的数据,实现父->子的组件通信 function changeToy() { console.log(c1.value); // c1.value 获取子组件的实例对象(可以访问到子组件通过defneExpose暴露的数据) c1.value.toy = "小猪佩奇"; } function changeComputer() { // c2 为Child2组件的实例对象 c2.value.computer = "DELL"; } // 向外部提供数据 defineExpose({ house }); </script>$refs在事件回调中传递
$refs获取ref标识的全部子组件实例对象vue<template> <div class="father"> <button @click="getAllChild($refs)">让所有孩子的书变多</button> <!-- $refs作为事件回调的参数进行传递,$refs是一个对象包含全部使用ref标识的子组件的实例对象 --> <!-- 为子组件添加ref属性 --> <Child1 ref="c1" /> <Child2 ref="c2" /> </div> </template> <script setup lang="ts" name="Father"> import Child1 from "./Child1.vue"; import Child2 from "./Child2.vue"; // 修改子组件中的数据,实现父->子的组件通信 function getAllChild(refs: object) { console.log(refs); // Proxy(Object) {c1: Proxy(Object), c2: Proxy(Object)} for (const ref of Object.values(refs)) { ref.book += 3; } } // 向外部提供数据 defineExpose({ house }); </script>$parent在事件回调中传递
$parent获取当前组件的父组件的实例对象vue<template> <div class="child1"> <h3>子组件1</h3> <button @click="minusHouse($parent)">干掉父亲的一套房产</button> </div> </template> <script setup lang="ts" name="Child1"> import { ref } from "vue"; let toy = ref("奥特曼"); let book = ref(3); // 修改父组件中的数据,实现 子组件->父组件间的通信 function minusHouse(parent: any) { console.log(parent); // 在子组件中操作父组件的数据 parent.house--; } // 使用defineExpose 暴露数据供付组件使用ref进行访问 defineExpose({ toy, book }); </script>
注意:
- 要想通过
ref获取的子组件实例对象访问子组件上的数据,需要在子组件中通过defineExpose暴露数据$refs、$parent只能事件回调中作为参数使用
Tips:读取 ref 定义的响应式数据是否需要 .value
- 读取
reactive定义的响应式对象内ref定义的属性,不需要.value - 读取
ref直接定义的响应式数据必须.value
let obj = reactive({
a: 1,
b: 2,
c: ref(3),
});
console.log(obj.a, obj.b);
console.log(obj.c); // 1.读取reactive定义的响应式对象内的ref定义的属性,不需要.value
let x = ref(4);
console.log(x.value);// 2.读取ref直接定义的响应式对象必须.value6.7 provide inject
描述:实现 祖↔任意后代组件 直接通信,无需借助父组件
具体使用:
- 在祖先组件中通过
provide配置向后代提供数据 - 在任意后代组件中通过
inject配置来声明接收数据
祖 -> 孙:祖先为其提供数据,孙 ->祖:祖先为其提供方法
具体编码:
在父组件中使用
provide提供数据语法:
provide(名, 值)vue<script setup lang="ts" name="Father"> import { provide, reactive, ref } from "vue"; import Child from "./Child.vue"; // 数据 let money = ref(100); let car = reactive({ brand: "奔驰", price: "199", }); // 方法 function updateMoney(value: number) { money.value -= value; } // 向后代提供数据和方法,ref定义的数据不要加.value provide("moneyContext", { money, updateMoney }); provide("car", car); </script>在后代组件中使用
inject配置接收数据语法:
inject(名,[默认值])vue<template> <div class="grand-child"> <h3>我是孙组件</h3> <h4>银子{{ money }}w</h4> <h4>车子:一辆{{ car.brand }}价值{{ car.price }}w</h4> <button @click="updateMoney(10)">花爷爷的money</button> </div> </template> <script setup lang="ts" name="GrandChild"> import { inject } from "vue"; // inject 获取祖先组件传递的值,可以使用默认值进行类型的推断 let { money, updateMoney } = inject("moneyContext", { money: 0, updateMoney: (n: number) => {}, }); console.log(money); // 获取的是Ref响应式对象,当祖先组件的数据发生改变了,这里也会自动更新 let car = inject("car", { brand: "未知", price: 0 }); </script>
注意:
- 在使用
provide提供数据时,要想inject接收到的数据响应父组件数据的变化就不能传递具体的值(eg:.value.xx接收到为一个值,而不是响应式数据),应为xx或xx.value这样子组件接收到的为一个响应式数据(原因:组件中的响应式数据发生变化会重新解析模板,但不会再次执行 js 代码,不会再次注入新的数据)- 在子组件中出现类型检查错误时,可以通过设置默认值的方式解决
6.8 pinia
见 五、pinia
6.9 slot
作用:让父组件可以向子组件指定位置插入 html结构,也是一种组件间通信的方式,适用于 父组件 <==> 子组件
分类:默认插槽、具名插槽、作用域插槽
使用场景:父组件向子组件传递带数据的标签,当一个组件有不确定的结构时, 就需要使用slot 技术
注意:
组件标签要写成双标签
插槽内容是在父组件中编译后,再传递给子组件的,因此插槽内元素的样式要写在父组件中。

6.9.1 默认插槽
父组件中:
在组件标签中添加要传递给子组件的 html 代码
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<!-- 插槽内容 -->
<Category title="热门游戏列表">
<ul>
<li v-for="game of games" :key="game.id">{{ game.name }}</li>
</ul>
</Category>
<Category title="今日美食城市">
<img :src="" data-missing="imgURL" alt="" />
</Category>
<Category title="今日影视推荐">
<video :src="" data-missing="videoURL" controls></video>
</Category>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import { reactive, ref } from "vue";
import Category from "./Category.vue";
// 数据
let games = reactive([
{ id: "asv1", name: "王者荣耀" },
{ id: "asv2", name: "英雄联盟" },
{ id: "asv3", name: "QQ飞车" },
{ id: "asv4", name: "地下城与勇士" },
]);
let imgURL = ref("https://z1.ax1x.com/2023/11/19/piNxLo4.jpg");
let videoURL = ref("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4");
</script>
// 样式
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
.content {
display: flex;
justify-content: space-evenly;
}
img,
video {
width: 100%;
}
</style>子组件中:
使用 slot 标签指定 html 代码展示的位置
<template>
<div class="category">
<h2>{{ title }}</h2>
<!-- 指定插槽的位置 -->
<slot>默认内容</slot>
</div>
</template>
<script setup lang="ts" name="Category">
defineProps(["title"]);
</script>注意:组件标签内的
html代码是在父组件中编译完成再传递给子组件的,因此样式需要写在父组件的<style>中
6.9.2 具名插槽
为每个插槽 <solt name="xxx"> 指定一个 name 属性用来区分不同的插槽,在父组件中使用 <template v-slot:name>来指定所要使用的插槽
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Category>
<!-- 将<template>内的内容放在<slot name="s1">标签处 -->
<template v-slot:s1>
<h2>热门游戏列表</h2>
</template>
<!-- 将<template>内的内容放在<slot name="s2">标签处 -->
<template v-slot:s2>
<ul>
<li v-for="game of games" :key="game.id">{{ game.name }}</li>
</ul>
</template>
</Category>
<Category>
<!-- 将<template>内的内容放在<slot name="s1">标签处 -->
<template #s1>
<h2>今日影视推荐</h2>
</template>
<!-- 将<template>内的内容放在<slot name="s2">标签处 -->
<template #s2>
<video :src="" data-missing="videoURL" controls></video>
</template>
</Category>
</div>
</div>
</template>子组件:
<template>
<div class="category">
<!-- 指定插槽的位置 -->
<slot name="s1">标题</slot>
<hr>
<!-- 具名插槽 -->
<slot name="s2">默认内容</slot>
</div>
</template>注意:
v-slot只能使用在<template>标签或组件标签上
Tips:
v-slot语法糖:<template v-slot:xx> === <template #xx>
6.9.3 作用域插槽
描述:数据在子组件中,但结构需要根据数据在父组件中生成(即数据在子组件中,父组件使用插槽生成的结构需要子组件中的数据) 子组件 ==> 父组件
此时会产生数据的作用域问题,需要使用作用域插槽来实现
使用:
子组件:
在
<slot>标签中使用props将数据传递给插槽的使用者
<template>
<div class="game">
<h2>热门游戏列表</h2>
<!-- slot 将数据传递给插槽的使用者 -->
<slot name="qwe" :games="games" a="哈哈我是作用域插槽"></slot>
</div>
</template>
<script setup lang="ts" name="Game">
import { reactive } from "vue";
// 数据
let games = reactive([
{ id: "asv1", name: "王者荣耀" },
{ id: "asv2", name: "英雄联盟" },
{ id: "asv3", name: "QQ飞车" },
{ id: "asv4", name: "地下城与勇士" },
]);
</script>- 父组件:通过在
<template v-slot="xx">接收<slot>传递的数据
<Game>
<!-- <template v-slot="xx"> 接收solt标签传递的props xx为一个包含所有props属性的对象 -->
<template #qwe="params">
<!-- <template #qwe="{games, a}"> -->
<span>{{ params }}</span><!-- {games: Array(4), a: '哈哈我是作用域插槽'} -->
<ul>
<li v-for="game of params.games" :key="game.id">{{ game.name }}
</li>
</ul>
</template>
</Game>v-slot:"xx" 与 v-slot="xx" 的区别:
v-slot:"xx"用于具名插槽中,用于区分不同的插槽,可以简写为#xxv-slot="xx"用于接收子组件的<slot>标签传递的数据
注意:
v-slot必须写在<template>标签中
v-slot="xx"xx为一个包含传递所有数据的对象,也可以对其进行解构作用域插槽
v-slot="xx"和具名插槽v-slot:xx一起使用时的写法vue<template v-slot:name="params"> 或 <template #name="params"> <!-- name为slot的名字,params为slot传递的数据--> .... </template>
七、其他API
7.1 shallowRef 与 shallowReactive
7.1.1 shallowRef
作用:创建一个响应式数据,但只对顶层属性进行响应式处理
用法:
jslet shallowPerson = shallowRef({...})特点:只跟踪引用值的变化,不关心值内部数据变化
7.1.2 shallowReactive
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
用法:
jslet shallowPerson = shallowReactive({...})特点:对象的顶层属性是响应式的,但嵌套对象的属性不是
总结:
通过使用 shallowRef() 和 shallowReactive()来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,这避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可以提升性能

7.2 readonly 与 shallowReadonly
7.2.1 readonly
作用:创建一个响应式数据的深只读副本
用法:
jsconst sum1 = ref(0); const sum2 = readonly(sum1);// readonly的参数必须为一个响应式数据 // sum2会随着sum1的变化而变化 const person1 = reactive({name:"张三",age:18}) const person2 = readonly(person1) sum2.value = 1;// 无法赋值因为sum2是只读的 person2.name = "李四" // 无法赋值因为person2是只读的特点:
- 对象的所有嵌套属性都变为只读
- 任何尝试修改这个对象的操作都会被阻止
const s2 = readonly(s1),s1发生改变时,s2也会随之改变
应用场景:
- 创建不可变的状态快照
- 保护全局状态或配置不被修改
7.2.2 shallowReadonly
作用:与
readonly类似,但只作用于对象的顶层属性用法:
jsconst car1 = reactive({ brand:"奔驰", options:{ color:"pink", price:18 } }) const car2 = shallowReadonly(car1) car2.brand = "小鸟" // 不可修改 car2.options.color = "black" // 可以修改特点:
- 只将对象的顶层属性(对象内的一级属性)设置为只读,对象内部的嵌套属性仍然是可变的
- 适用于只需要保护对象顶层属性的场景
7.3 toRaw 与 markRow
7.3.1 toRaw
作用:用于获取一个响应式对象的原始对象,
toRow返回的对象不再是响应式的,不会触发视图更新官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
使用:
jsconst person = reactive({ name: "Tony", age: 18, }); // 用于获取一个响应式对象的原始对象 let person2 = toRaw(person); console.log("响应式数据", person); // Proxy(Object) {name: 'Tony', age: 18} console.log("原始数据", person2); // {name: 'Tony', age: 18}何时使用? —— 在需要将响应式对象传递给非
Vue的库或外部系统时,使用toRaw可以确保它们收到的是普通对象
7.3.2 markRow
作用:标记一个对象,使其永远不会变成响应式的
例如使用
mockjs时,为了防止误把mockjs变为响应式对象,可以使用markRaw去标记mockjs使用:
js// markRaw:标记一个普通对象,使其永远不会变成响应式对象 let car = markRaw({ brand: "奔驰", price: 100, }); let car2 = reactive(car); console.log("car:", car); // {brand: '奔驰', price: 100} console.log("car2:", car2); // {brand: '奔驰', price: 100}
7.4 customRef
作用:创建一个自定义
ref,当自定义响应式数据发生变化时可以增加一些逻辑的控制使用:
利用
customRef实现响应式数据发生变化后一秒页面再进行更新
// 使用vue默认提供的默认ref定义响应式数据,数据一变化,页面就更新
let msg = ref("你好");
// 使用自定义ref去定义响应式数据
let initValue = "你好"
let timer:number
// track:跟踪 trigger:触发
let msg = customRef((track,trigger)=>{
return{
get(){// 当msg被读取时调用
track() //告诉Vue数据msg很重要,要对msg进行持续关注,一旦msg发生变化就去更新
return initValue
}
set(value){
// 实现数据修改一秒后再更新数据
clearTimeout(timer)
timer = setTimeout(()=>{
initValue = value // 修改数据
trigger() // 通知Vue数据msg变化了
},1000)
}
}
})注意:
track()和trigger()的作用
track():告诉Vue自定义的响应式数据很重要,要对其持续关注,一旦发生变化就去更新trigger():通知Vue自定义响应式数据变化了
将
customRef定义的响应式数据msg封装为一个hooksuseMsgRef.ts
// 将自定义ref msg 封装为一个hooks
import { customRef } from "vue";
export default function (initValue: string, delay: number) {
// 使用自定义ref(customRef)去定义响应式数据
let timer: number;
// track:跟踪 trigger:触发
let msg = customRef((track, trigger) => {
return {
// get何时被调用:msg被读取时调用
get() {
track(); // 告诉Vue数据msg很重要,你要对msg进行持续关注,一旦msg变化就去更新
return initValue;
},
// set何时被调用:msg被修改时调用
set(value) {
console.log("msg被修改了", value);
// 实现一秒钟后再更新数据
clearTimeout(timer);
timer = setTimeout(() => {
initValue = value; // 修改数据
trigger(); // 通知Vue数据msg变化了
}, delay);
},
};
});
return { msg };
} 使用:
// 使用useMsgRef来定义一个响应式数据且有延迟效果
const { msg } = useMsgRef("你好", 1000);八、Vue3新组件
8.1 Teleport
Teleport 是一种能将我们组件中的html结构移动到指定位置的技术
用法:<teleport to="xx">html</teleport> xx 可以为任意标签选择器
使用场景:当组件内的html结构受到了其他原因的影响,必须移动到指定的位置时,就可以使用 Teleport 标签
eg:使用 Teleport解决 CSS 滤镜影使 fix 定位不参照浏览器页面的问题
filter: saturate(0%); /* 网站置灰 滤镜*/
/* filter: saturate(200%); 色彩增强*/<template>
<button @click="isShow = true">展示弹窗</button>
<!-- 由于App组件使用了滤镜导致弹窗的定位不参照浏览器页面 -->
<teleport to='body' > <!-- 使用Teleport使指定html结构传送到指定位置 -->>
<!-- teleport传送门 to可以为CSS选择器 -->
<div v-show="isShow" class="modal">
<h2>我是弹窗的标题</h2>
<p>我是弹窗的内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
</template>8.3 suspense
作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验
使用场景:子组件中有异步任务,且在网速慢加载时,让页面显示一些东西时可以使用 suspense
使用步骤:
使用
Suspense包裹有异步任务的组件,并配置好default与fallback两个插槽default:用于展示异步组件fallback:用于异步组件未加载出来时进行展示
Child.vue 子组件:存在异步任务需要等请求成功后才会渲染页面
<script setup lang="ts" name="Child">
import axios from "axios";
import { ref } from "vue";
let sum = ref(0);
// 存在异步任务 await需要写在async函数中,setup自带async函数
let {data: { content },} = await axios.get("https://api.uomg.com/api/rand.qinghua?format=json");
console.log(content);
</script>App.vue 父组件:使用 suspense 在等待异步组件时渲染一些额外内容,让应用有更好的用户体验
<template>
<div class="app">
<h2>我是App组件</h2>
<!-- 使用 Suspense 组件包裹子组件,当子组件加载失败时,会显示一个loading组件 -->
<Suspense>
<!-- 内部有两个插槽,default和fallback -->
<template #default>
<Child />
</template>
<template #fallback>
<h2>加载中....</h2>
</template>
</Suspense>
</div>
</template>8.3 全局 API 转移到应用对象
Vue2 中的 Vue.xx 变成了 app.xx
| API | 作用 | 描述 |
|---|---|---|
app.component | 注册全局组件 | 在任意一个组件中都可以直接该组件标签使用 |
app.config | 全局配置对象 | 设置全局的配置 |
app.directive | 注册全局种指令 | 使自定义指令在全局可用 |
app.mount | 挂载应用 | 将应用挂载到指定的html元素的位置 |
app.unmount | 卸载应用 | |
app.use | 安装插件 | 使用 Router,Pinia |
8.4 其他
过渡类名
v-enter修改为v-enter-from、过渡类名v-leave修改为v-leave-from。keyCode作为v-on修饰符的支持。v-model指令在组件上的使用已经被重新设计,替换掉了v-bind.sync。v-if和v-for在同一个元素身上使用时的优先级发生了变化。v-if优先级比v-for更高,因此不能使用v-if来控制v-for渲染元素,推荐使用v-show移除了
$on、$off和$once实例方法。移除了过滤器
filter。移除了
$children实例propert。......