目录
3.2.3 (面试题)请你谈谈 setup 和 Options API 之间的关系
6.3 mitt——消息订阅与发布(与pubsub类似的中间人)
6.5 $attrs实现祖孙通信——主导权永远在祖先——需要搭中间人
7.1 shallowRef 与 shallowReactive——浅层响应式数据
7.2 readonly 与 shallowReadonly——只读
7.3 toRaw 与 markRaw —— 原始/永久原始
一、Vue3 VS Vue2
1.1 Vue3的简介
Vue3是Vu2后一个大版本,从整体来看,Vue3很好的兼容了Vue2中所有的特性,与此同时Vue3还创造出了其他优秀、便捷的特性,例如:组合式API、setup语法糖、支持TypeScript等等,本文适用的人群:
1. 如果你是想快速上手前端框架,了解Vue的使用方式的人群
2. 想要重新回顾vue语法,或是刚刚学完vue2,想要继续学习更新的Vue3语法的人群
当然,如果你有足够的时间和兴趣,我还是十分建议你从vue2的用法看起。毕竟如果以后需要维护vue相关的项目,还是有很多项目任然在使用vue2。那你的学习路线可以是:
1. 学习axios【vue中发送请求】:Axios中文文档 | Axios中文网 (axios-http.cn)
2. 系统学习vue2:尚硅谷Vue2.0 vuejs从入门到精通_哔哩哔哩_bilibili
1.2 Vue3的优化
性能的提升——更快更轻量
- 打包大小减少41%
- 初次渲染快55%,更新渲染快133%
- 内存减小54%
源码升级
- 使用Proxy 代替 defineProperty 实现响应式
- 重写虚拟Dom的实现和 Tree-Shaking
新特性
- 组合式API 取代 选项式API
- setup语法糖
- ref 与 reactive
- computed 和 watch
- 新的生命周期钩子
onBeforeUpdate
和onUpdated
【补充说明】
在Vue 2.x中,Vue的响应式系统是通过
Object.defineProperty
实现的,这种方法有一些限制,比如不能监听属性的添加或删除,也不能很好地处理数组和嵌套对象。而在Vue 3.x中,Vue团队采用了Proxy
来替代Object.defineProperty
实现响应式系统。
- Object.defineProperty:这种方式只能劫持对象的属性,不能监听数组索引的变化,且需要在对象属性被访问和修改时拦截它们。
- Proxy:可以创建一个对象的代理,从而可以基本上拦截并定义对象属性的基本操作,如属性查找、赋值、枚举、函数调用等。Proxy 不仅可以拦截属性的读取和设置,还可以拦截属性的添加、删除以及数组的索引操作等,更加全面和灵活。
- 在前端框架中应用Tree-Shaking:这意味着框架的库文件或模块被设计为支持Tree-Shaking,使得开发者在引入框架的某些部分时,可以只包含这些部分的实际代码,而不包含整个库的其他未使用的部分。这对于减少最终应用的体积非常有帮助,尤其是在使用现代打包工具时。
- 重写虚拟DOM:这可能意味着对现有框架的虚拟DOM实现进行优化,以提高性能、减少内存占用或改进API的使用体验。例如,改进算法以更高效地比较和更新DOM树,优化节点的创建和销毁过程,或者提供新的特性如更细粒度的更新控制。
二、Vue3 工程的创建
2.1 基于 vue-cli进行构建 (耗时,不推荐!)
这种方式在创建vue2项目时就使用了,目前vue-cli 已经处于维护模式了,而官方也不推荐这种实现方式。创建一个项目 | Vue CLI
输入指令,选择创建版本、包管理器:
开始创建项目:耗时26S + 5S
启动项目: 耗时5.5s
2.2 基于vite创建(推荐)
第一中构建方式耗时特别长,官方推荐使用新一代前端构建工具—基于vite构建
vite的优势 Home | Vite中文网 (vitejs.cn)
1. 轻量快速的热重载(`HMR`),能实现极速的服务启动。
2.对 `TypeScript`、`JSX`、`CSS` 等支持开箱即用。
3.真正的按需编译,不再等待整个应用编译完成。
vue-cli (webpack)构建 与 vite 构建的对比
从对比图也可以发现,vite采用的懒汉式,可以使得构建速度大大提高。
输入指令,选择配置,创建项目: 一瞬间就完成了
初始化工程,启动项目
2.3 vue3项目结构说明
1. Vite项目中,index.html 是项目的入口文件,在项目最外层。
2. 加载index.html后,Vite解析 `<script type="module" src="xxx">` 指向的JavaScript。
3.Vue3中是通过 createApp函数创建一个应用实例。4.Vue3向下兼容Vue2语法,且Vue3中的模板中可以没有根标签
2.4 一个简单的vue程序
2.4.1 如何在根组件中联动子组件
1. 导入组件
2. 使用 components 注册组件
3. 在模板中使用组件
2.4.2 编写一个vue程序
App.vue
<template>
<div class="app">
<h1>你好啊!</h1>
<!--3. 使用组件-->
<person></person>
</div>
</template>
<script lang="ts">
// 1. 导入组件
import person from './components/person.vue';
export default {
name:'App', //组件名
components:{person} //2. 注册组件
}
</script>
<style>
.app {
background-color: #ddd;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
</style>
person.vue
<template>
<div class="person">
<h2>姓名: {{name}}</h2>
<h2>年龄: {{age}}</h2>
<button @click="showTel()">查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name: 'person',
data() {
return {
name: 'zhicong',
age: 18,
tel: '123456789'
}
},
methods: {
showTel() {
alert(this.tel)
}
}
}
</script>
<style scoped>
.person {
background-color: skyblue;
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
}
</style>
三、Vue3的核心语法
3.1 组合式API 和 选项式API
如图所示:
左边是vue2采用的选项式API的写法。也就是采用 data、methods、computed分开来写,导致同一个功能的逻辑被拆散成多块【同一颜色表示一个功能】,这样子在进行项目开发的过程中会比较难维护,功能数据、方法、计算属性之间的跨度大。
右边采用的是vue3新增的组合式API的写法,它允许同一功能的数据、方法、计算属性写在同一块,使得项目的可读性与可维护性增强。因此我们学习vue3,正是要学习组合式API开发。
组合式API的优势:
1. 代码组织性提升
- 逻辑集中化:组合式API允许开发者将相关的逻辑(如响应式数据、计算属性、方法等)封装在
setup
函数中,从而避免了传统选项式API中因逻辑分散在data
、computed
、methods
等选项中而导致的管理复杂性。- 结构清晰度:通过组合式API,组件的逻辑可以根据功能或用途进行自然分组,而不是依赖于Vue的内置选项。
2. 类型推断的增强
- TypeScript友好:组合式API与TypeScript的集成更加紧密,能够充分利用TypeScript的类型系统。开发者可以在编写Vue组件时享受到更强大的类型检查和自动完成功能,从而提高开发效率和代码质量。
3. 代码复用性的增强
- 可复用逻辑封装:组合式API使得将可复用的逻辑(如API调用、状态管理等)封装成独立的函数或对象变得更加容易。这些函数或对象可以在多个组件之间共享,从而减少了代码的重复,提高了代码的可维护性。
3.2 初识setup语法
为了能够实现组合式API,vue3新增了一个配置项——setup。组件中所使用的 数据、方法、计算属性、监视等,都需要写在setup之中。
3.2.1 朴素的setup写法
setup是组合式API的入口,在组件创建前执行。在使用的过程中,你可以在setup函数中定义需要的响应式状态、方法函数、计算属性、监视......然后将需要用到的部分通过返回值返回,才能在模板中被使用。
<template>
<div>{{ name }}</div>
<div>{{ age }}</div>
</template>
<script>
import { ref } from 'vue';
export default {
// 书写setup函数,在函数中定义需要用到的响应式状态、方法、计算属性,并返回它们
setup() {
// 定义了一个响应式的变量
const name = ref('zhicong');
const age = ref(18)
// 返回的对象中的属性和函数在模板中可用
return { name,age };
}
}
</script>
3.2.2 setup语法糖写法
有没有觉得现在需要多写一个setup函数有点麻烦,而且用到什么都必须手动返回,不然就没办法在模板中使用。如果以后需要返回的变量和方法特别多,就十分麻烦。对此,Vue3提供了一种语法糖写法,很好的解决了上述问题:它可以不需要你写setup函数、并且自动帮你返回对象。
注意:语法糖写法通常会出现两个script标签,不影响,一个专门导入组件、一个专门写数据、方法、逻辑。
<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>
3.2.3 (面试题)请你谈谈 setup 和 Options API 之间的关系
1. setup 与 data、methods 等是可以混合使用的,也就是在写Vue3的同时兼容Vue2
2. setup 在所有钩子函数之前就执行了,因此data、methods是可以访问setup中的属性的
3. setup无法访问data、methods之中的配置
4. setup函数中,this指代的是 Undefined,因此我们基本上不会在setup中使用this
3.3 创建响应式数据——ref 和 reactive
在vue2中,只要将数据写在data中,数据就是响应式的。而在vue3中,需要通过ref 和 reactive来定义响应式数据
【使用步骤】先引入后使用
3.3.1 基于ref创建响应式数据
1. 使用ref创建基本类型的响应式数据
使用ref创建基本类型的响应数据,在浏览器控制台中就会发现ref会将数据封装成一个对象。其中的value的值就是我们原本的数据。因此,我们想要使用数据,必须要利用对象.value才能获得。
在模板中,vue已经提前帮我们做好了,所以我们不需要做这一步,但是在script中必须不能忽略这一步。
2. 使用ref创建对象类型的响应式数据
3.3.2 基于reactive创建对象类型的响应式数据
使用reactive创建对象响应式数据,打开浏览器控制台会发现对象被vue包裹成了一个代理对象,与ref不同的是,这个对象可以直接取到值。不需要.value。
reactive是深层次的,不管被包裹的对象层次有多深,都能变成响应式数据。
不能定义基本类型,会报错
修改对象数据时,必须整个对象进行深拷贝,如果单单修改数据,或者修改完数据又加一层reactive都是不行的。
3.3.3 ref 与 reactive的比较
宏观角度:
ref 用来定义:基本数据类型、对象数据类型
reactive 用来定义:对象数据类型
区别:
ref 创建的变量必须使用 .value (可以使用 volar 插件自动添加 .value)
reactive 重新分配一个新对象,会失去响应式 (可以使用 Object.assign 去整体替换)
使用原则:
- 若需要一个基本类型的响应式数据,必须使用 ref
- 若需要一个响应式对象,层级不深,ref、reactive 都可以
- 若需要一个响应式对象,且层级较深,推荐使用 reactive
3.4 toRefs 和 toRef的用法
【使用场景】将一个响应式对象的每个属性,转换成为一个 ref 对象。其中toRefs可以批量替换
3.5 computed的用法
【作用】根据已有数据,计算出新的数据
【优势】计算属性相比于函数实现,有缓存,对于相同数据的重复请求,并不会重新调用一次,从而提高了性能。
看看vue官方怎么说:
3.6 Watch的用法
监视,用于监视数据的变化。可以监视以下4种数据:
1. ref定义的数据
2. reactive定义的数据
3. 函数返回一个值 监视getter函数
4. 一个包含上述内容的数组
情况1:监视【ref】定义的【基本类型】数据
// 情况一:监视【ref】定义的 【基本类型】数据
let sum = ref(0);
const stopWatch = watch(sum, (newVal, oldVal) => {
console.log(newVal, oldVal);
//这个watch还返回一个函数,解除监视
if (newVal > 10) {
stopWatch();
}
});
console.log(stopWatch); //返回一个函数
监视的基本语法:
取消监视:使用变量接收,监听条件达标执行stopWatch()函数取消监听
情况2:监视【ref】定义的【对象类型】数据
let person = ref({
name: "张三",
age: 18,
});
const changeName = () => {
person.value.name += "~";
};
const changeAge = () => {
person.value.age += 1;
};
const changePerson = () => {
person.value = { name: "李四", age: 20 };
};
//监视【ref】定义的 【对象类型】数据,监视的是对象的地址值,
// 若想监视对象内部的属性变化,需要手动开启深度监视。
watch(
person,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true }
);
默认直接监视整个对象
这种情况监视的是对象的地址值,只有对象的地址值发生了改变,才会被监视到。内部个别数据改变不会被监视。
手动开启深度监视
开启深度监视后,内部数据变化都会被监视到。
情况3: 监视【reactive】定义的【对象类型】数据,默认隐式开启深度监视
let p = reactive({
name: "张三",
age: 20,
});
const pName = () => {
p.name += "~";
};
const pAge = () => {
p.age += 1;
};
const pPerson = () => {
//这里不是真正意义上的修改整个人,而是进行了批量的修改!!!
Object.assign(p, { name: "李四", age: 19 });
};
//监视【reactive】定义的 【对象类型】数据,默认开启了深度监视!!!
watch(p, (newVal, oldVal) => {
console.log(newVal, oldVal);
});
【如何修改reactive定义的数据】
注意reactive定义的对象数据不能整体修改,需要使用obiect.assign 进行深拷贝。而且,这种拷贝方式并没有修改对象的地址值。
默认监视时,数据发生改变就能监视到【区别于ref】
隐式创建了深度监视,无法关闭
情况4: 只想监视ref 或 reactive定义的【对象类型】的某个属性
let p2 = reactive({
name: "xiaoyu",
age: 18,
car: {
c1: "奔驰",
c2: "宝马",
},
});
const p2Car = () => {
p2.car = { c1: "雅迪", c2: "飞机" };
};
//情况四:监视响应式对象中的某个属性,且该属性是基本数据类型,要写成函数式。
watch(
() => p2.name,
(newVal, oldVal) => {
console.log(newVal, oldVal);
}
);
//下面这个情况就很诡异,能监视到对象里面的值变化,对象本身却不进行监视!
// watch(p2.car,(newVal,oldVal)=>{
// console.log(newVal,oldVal);
// })
//建议写成监视其中的属性任然是一个对象类型的,也推荐使用函数形式的!
watch(
() => p2.car,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true }
);
1. 若属性不是对象类型,则需要写成函数形式
写箭头函数进行返回需要监视的属性
2. 若属性依然是对象类型,可以直接编写,但是也建议写出函数形式
情况5: 监听多个数据
使用函数返回 :
由于车是对象属性,也可以这么写:
3.7 WatchEffect的用法
当watch如果要监视的数据变得特别多才能判断的时候,要写就比较麻烦了,这个时候我们可以使用WatchEffect去替换watch。
watch
对比watchEffect
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
:要明确指出监视的数据
watchEffect
:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
具体的,WatchEffect的作用为: 自动监视需要监视的对象,无需指名道姓。
3.8 标签的ref属性
作用:用于注册模板引用
- 用于普通 DOM 标签上,获取的是 DOM 节点
- 用于组件标签上,获取的是组件实例对象
- 比id标签好,id标签在不同组件可能会重复导致犯病,但是ref能确保组件唯一
注意,在vue3中,父组件想要获取子组件的东西时,必须要经过子组件同意(暴露)
用在普通DOM
标签上:
<template>
<div class="person">
<h1 ref="title1">尚硅谷</h1>
<h2 ref="title2">前端</h2>
<h3 ref="title3">Vue</h3>
<input type="text" ref="inpt"> <br><br>
<button @click="showLog">点我打印内容</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref} from 'vue'
// 声明ref空对象
let title1 = ref()
let title2 = ref()
let title3 = ref()
function showLog(){
// 通过id获取元素
const t1 = document.getElementById('title1')
// 打印内容
console.log((t1 as HTMLElement).innerText)
console.log((<HTMLElement>t1).innerText)
console.log(t1?.innerText)
/************************************/
// 通过ref获取元素
console.log(title1.value)
console.log(title2.value)
console.log(title3.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>
3.9 ts语法回顾
ts用于规范
1. 定义ts规范
2. 导入使用ts规范
3. 查看效果
4. 拓展多个
5. 自定义类型
6. 定义响应式对象——传泛型参数
3.10 (todo)props的用法
props用于父组件向子组件传递数据。主要用法:
1. 在子组件中,你需要通过组件的 props
选项来声明你想要从父组件接收哪些数据。
<!-- ChildComponent.vue -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
props: ['message'] // 声明一个名为 message 的 prop
}
</script>
2. 在父组件中,你需要引入并使用子组件,并通过在子组件的标签上添加自定义属性(这些属性的名称需要与子组件中声明的 props
名称相匹配)来传递数据。
<!-- ParentComponent.vue -->
<template>
<div>
<ChildComponent :message="parentMessage" />
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Hello from Parent!'
}
}
}
</script>
注意:在父组件的模板中,向子组件传递
props
时,你应该使用v-bind
指令(或简写为:
)来绑定数据。这样,Vue就知道你正在传递一个JavaScript表达式到子组件的props
中,而不是一个普通的字符串。
3. 最后,在子组件的模板或逻辑中,你可以像使用普通数据属性一样使用通过 props
接收到的数据。
<!-- ChildComponent.vue -->
<template>
<div>
<!-- 使用 props 中的 message -->
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
props: ['message'] // 声明 prop
}
</script>
4. Vue 允许你为每个 prop
指定类型、默认值、验证函数等,这有助于确保传递给子组件的数据是有效的。
<script>
export default {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 带有默认值的类型检查
propB: {
type: String,
default: 'default value'
},
// 自定义验证函数
propC: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['option1', 'option2'].includes(value)
}
},
// 带有默认值的对象
propD: {
type: Object,
// 对象或数组默认值必须从一个工厂函数返回
default: function () {
return { message: 'hello' }
}
}
}
}
</script>
3.11 生命周期
【概念】每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
【规律】生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
【vue2的生命周期函数】
创建阶段:
beforeCreate
、created
挂载阶段:
beforeMount
、mounted
更新阶段:
beforeUpdate
、updated
销毁阶段:
beforeDestroy
、destroyed
【vue3的生命周期函数】
创建阶段:
setup
挂载阶段:
onBeforeMount
、onMounted
更新阶段:
onBeforeUpdate
、onUpdated
卸载阶段:
onBeforeUnmount
、onUnmounte
【注册周期钩子】
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log(`the component is now mounted.`)
})
</script>
【最常用的周期钩子】onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)
3.12 自定义钩子函数 hook
为什么要有hook,先看下列现象:
现在我们写的vue3逻辑:实际上,和vue2选项式API还是很像:
hook:让相同功能的数据和方法合在一起,封装成模块化。
使用时,只需要获取自定义hook函数的返回值即可
注意:hook里面还可以写钩子、计算属性
四、Vue3路由理解
4.1 对路由的理解
路由就是进行页面切换规则匹配的组件。在我们进行单页面开发时,往往需要对某个导航页中的内容进行切换,这种切换并不是对整个页面的切换,而是对页面中特定的组件进行切换。至于如何指定特定的组件,这就需要用到我们的路由器,去进行路由规则的匹配了。
4.2 路由的基本使用步骤
1. 创建router文件夹,在文件夹下创建index.ts文件,编辑路由参数和配置路由规则,并将路由进行暴露
2. 配置main.ts 文件,将路由器进行引入并注册挂在到app容器之中
3. 在App.vue中使用配置好的路由器,具体的,在需要跳转路由的导航栏使用RouterLink配置跳转规则,在需要展示路由组件的位置 使用 RouterView进行标记声明
4. 编写路由组件
区分路由组件和一般组件的方法。很简单,路由组件我们一般都会用 RouterLink标签去配置它的路由路径和匹配规则,而一般组件的标签一般都是直接写组件名字。我们习惯性把路由组件统一放到pages / view 文件夹下,而把一般组件放到 component文件夹下。
【注意】
1. 路由组件通常放在pages 或 view文件夹中,一般组件通常存放在 components文件夹中
2. 通过点击导航,视觉上消失的路由组件,默认是被卸载掉的,只有需要的时候才会去挂载
4.3 路由工作模式
路由分成两种工作模式,各有其特点。我们前面演示的时候使用的是 history模式。除此之外,还有hash模式。具体的:
【history模式】
优点: URL美观,不带有#,更加接近传统的网页URL,一般多见于前台界面
缺点:后期项目上线,需要服务端额外处理配置来配合解决路径问题,否则刷新会显示404错误
用法:
const router = createRouter({
history:createWebHistory(), //history模式
/******/
})
【vue2】 mode : ‘history’
【vue3】 history :createWebHistory()
【React】 BrowserRouter
【hash模式】
优点:兼容性更好,不需要服务器的额外配置,一般多见于后台界面
缺点:URL带有#不太美观,而且在SEO优化方面相对较差
用法:
const router = createRouter({
history:createWebHashHistory(), //hash模式
/******/
})
4.4 RouterLink中 to属性的两种写法
<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>
<!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>
给路由规则命名
routes:[
{
name:'zhuye',
path:'/home',
component:Home
},
{
name:'xinwen',
path:'/news',
component:News,
},
{
name:'guanyu',
path:'/about',
component:About
}
]
<!--简化前:需要写完整的路径(to的字符串写法) -->
<router-link to="/news/detail">跳转</router-link>
<!--简化后:直接通过名字跳转(to的对象写法配合name属性) -->
<router-link :to="{name:'guanyu'}">跳转</router-link>
4.5 二级路由【嵌套路由】
如果要编写二级路由,实现下列的效果:
1. 配置子级路由
2. 配置路由使用路径和路由组件展示区
4.6 嵌套路由——query参数
嵌套路由种,我们常常需要进行传参。因此我们需要用以下两种方法,对RouterLink进行改造:
4.7 嵌套路由——params参数
1. 需要先在子路由中占位:
2. 配置传参路径
3. 使用
4. 如何可选传参,占位了但是有些时候不需要用上
注意:
params有一些使用限制
1. 传递params参数时,如果to使用的是对象写法,必须使用name配置项,不能使用path
2. 传递params参数时,必须提前在规则中进行占位
3. 对于可选参数,必须在规则中声明 ' ? '
4.8 路由的props配置
现在写法还是有些麻烦,体现在
如何简化这里的写法,我们可以参考组件的props,使用props简化这部分的传参
4.9 replace模式
1. 作用:控制路由跳转时操作浏览器历史记录的模式。
2. 浏览器的历史记录有两种写入方式:分别为```push```和```replace```:
- ```push```是追加历史记录(默认值)。
- `replace`是替换当前记录。
3. 开启`replace`模式:
<RouterLink replace .......>News</RouterLink>
4.10 编程式导航跳转
4.11 路由重定向
{
path:'/',
redirect:'/home'
}
五、pinia——集中式状态管理工具
Vue全家桶 - pinia 的理解和学习1(Pinia 核心概念的 Store、State、Getter、Action)_pinia getters和actions-CSDN博客
5.1 pinia环境搭建
第一步:使用命令 npm i pinia 进行安装
第二步:导入pinia
1. 导入createPinia
2. 创建pinia
3. 挂载使用pinia
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')
5.2存储 + 读取 数据
1. `Store`是一个保存:**状态**、**业务逻辑** 的实体,每个组件都可以**读取**、**写入**它。
2. 它有三个概念:`state`、`getter`、`action`,相当于组件中的: `data`、 `computed` 和 `methods`。
3. 具体编码:`src/store/count.ts`
// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useCountStore = defineStore('count',{
// 动作
actions:{},
// 状态
state(){
return {
sum:6
}
},
// 计算
getters:{}
})
4. 具体编码: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:{}
})
5. 组件中使用`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>
5.3 修改数据的三种方式
第一种修改方式,直接修改
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)
5.4 解构响应式数据——storeToRefs
1. 借助storeToRefs将store中的数据转为ref对象,方便在模板中使用。
2. 注意: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>
注意:storeToRefs 解构 store 中的 状态 或 getter,跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性。
action 或 非响应式 (不是 ref 或 reactive) 的属性 直接在 Store 中解构。
5.5 getters
1. 概念:当state中的数据,需要经过处理后再使用时,可以使用getters配置。
2. 追加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)
5.6 subscribe方法的使用
通过 store 的 $subscribe()
方法侦听 state
及其变化。当store中的state变化到我们想要的那个值时,我们需要去做些什么,那么我们就需要用到$subscribe。相比于watch,使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次。
talkStore.$subscribe((mutate,state)=>{
console.log('LoveTalk',mutate,state)
localStorage.setItem('talk',JSON.stringify(talkList.value))
})
5.7 store组合式的写法
六、Vue3组件通信
组件之间互相传递数据,可能是父子、爷孙等不同的通信关系。
Vue3
组件通信和Vue2
的区别:
-
移出事件总线,使用
mitt
代替。
-
vuex
换成了pinia
。 -
把
.sync
优化到了v-model
里面了。 -
把
$listeners
所有的东西,合并到$attrs
中了。 -
$children
被砍掉了。
常见搭配形式:
6.1. props实现父子通信——主导权永远在父亲
初始化代码
父亲传递数据给儿子——属性值是非函数
很直接,只需要父亲将声明的数据发给儿子,儿子接收了就可以使用
孩子传递数据给父亲——属性值是函数
主导权还是在父亲手上,父亲需要先给孩子传递一个函数方法,孩子接收该方法,然后再调用该方法,把需要传递的参数通过该方法传递给父亲
6.2 自定义事件——子传父
1. 概述:自定义事件常用于:**子 => 父。**
2. 注意区分好:原生事件、自定义事件。
2.1 原生事件:
事件名是特定的(click、mosueenter等等)
事件对象$event: 是包含事件相关信息的对象(pageX、pageY、target、keyCode)
2.2 自定义事件:
事件名是任意名称
事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!
6.3 mitt——消息订阅与发布(与pubsub类似的中间人)
1. 安装mitt
npm i mitt
2. 新建文件,引入mitt,并暴露出去
// 引入mitt
import mitt from "mitt";
// 创建emitter
const emitter = mitt()
/*
// 绑定事件
emitter.on('abc',(value)=>{
console.log('abc事件被触发',value)
})
emitter.on('xyz',(value)=>{
console.log('xyz事件被触发',value)
})
setInterval(() => {
// 触发事件
emitter.emit('abc',666)
emitter.emit('xyz',777)
}, 1000);
setTimeout(() => {
// 清理事件
emitter.all.clear()
}, 3000);
*/
// 创建并暴露mitt
export default emitter
3. 接收数据的组件中:绑定事件、同时在销毁前解绑事件:
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";
// 绑定事件
emitter.on('send-toy',(value)=>{
console.log('send-toy事件被触发',value)
})
onUnmounted(()=>{
// 解绑事件
emitter.off('send-toy')
})
4. 提供数据的组件,在合适的时候触发事件
import emitter from "@/utils/emitter";
function sendToy(){
// 触发事件
emitter.emit('send-toy',toy.value)
}
6.4 v-model 数据双向绑定实现双向通信
v-model用在html标签上
v-model是语法糖:是:value 和 @input标签的语法糖写法,实现双向绑定
回顾v-model的本质
<!-- 使用v-model指令 -->
<input type="text" v-model="userName">
<!-- v-model的本质是下面这行代码 -->
<input
type="text"
:value="userName"
@input="userName =(<HTMLInputElement>$event.target).value"
>
v-model用在组件标签上
参考v-model的本质,我们在vue3中可以写出以下代码实现相关功能:
vue3 v-model 底层命令的名称和 vue2 的不一样
v-model后面可以指定名字,同步到底层组件时:@update:abc
<AtguiguInput v-model:abc="userName" v-model:xyz="password"/>
6.5 $attrs实现祖孙通信——主导权永远在祖先——需要搭中间人
1. 概述:$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙 / 孙→祖)。
2. 具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。
> 注意:$attrs会自动排除props中声明的属性(可以认为声明过的 props被子组件自己“消费”了)
实现组给孙传递数据
实现孙给祖修改数据
和props一样,传递方法就可以了,省略不写了。主导权永远在祖先!
6.6 $refs、$parent
1. 概述:
- $refs用于 :父→子。
- $parent用于:子→父。
2. 原理如下:
属性 | 说明 |
$refs | 值为对象,包含所有被`ref`属性标识的`DOM`元素或组件实例。 |
$parent | 值为对象,当前组件的父组件实例对象 |
父亲修改儿子的数据
使用Ref——获取一个儿子对象
升级$refs——获取所有的子组件对象
$parent 拿父亲数据
6.7 provide、inject——不需要搭中间人
1. 概述:实现祖孙组件直接通信
2. 具体使用:
- 在祖先组件中通过provide配置向后代组件提供数据
- 在后代组件中通过inject配置来声明接收数据
注意
在给后代传数据的时候,不能传 money.value,这个不是响应式的!!!
6.8 slot 插槽的基本使用
简单来说,就是模板。插槽部分可以自定义内容
6.8.1 默认插槽
使用slot标签声明插槽位,可以将子组件标签内的内容声明到插槽内
6.8.2 具名插槽
当需要多个不同的插槽作为填充位时,需要给插槽进行命名调用
6.8.3 作用域插槽
假设对于同一个数据的插槽,需要不同的样式、不同的展示方式。例如下面说的:第一个要求无序列表、第二个要求有序列表、第三个要求不是列表。
思考过程:
七、其他API
7.1 shallowRef 与 shallowReactive——浅层响应式数据
shallowRef
-
作用:创建一个响应式数据,但只对顶层属性进行响应式处理。
-
用法:
let myVar = shallowRef(initialValue);
-
特点:只跟踪引用值的变化,不关心值内部的属性变化
shallowReactive
-
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
-
用法:
const myObj = shallowReactive({ ... });
-
特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。
总结
通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式
API
创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
7.2 readonly 与 shallowReadonly——只读
readonly
-
作用:用于创建一个对象的深只读副本。
-
用法:
const original = reactive({ ... }); const readOnlyCopy = readonly(original);
-
特点:
- 对象的所有嵌套属性都将变为只读。
-
任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
-
应用场景:
- 创建不可变的状态快照。
-
保护全局状态或配置不被修改。
shallowReadonly
-
作用:与
readonly
类似,但只作用于对象的顶层属性。 -
用法:
const original = reactive({ ... }); const shallowReadOnlyCopy = shallowReadonly(original);
-
特点:
-
只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
- 适用于只需保护对象顶层属性的场景。
7.3 toRaw 与 markRaw —— 原始/永久原始
toRaw
-
作用:用于获取一个响应式对象的原始对象,
toRaw
返回的对象不再是响应式的,不会触发视图更新。官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
何时使用? —— 在需要将响应式对象传递给非
Vue
的库或外部系统时,使用toRaw
可以确保它们收到的是普通对象
2.使用场景: 有时候希望数据改了但是页面不会渲染,这个时候可以使用原始数据
markRaw
-
作用:标记一个对象,使其永远不会变成响应式的。
例如使用
mockjs
时,为了防止误把mockjs
变为响应式对象,可以使用markRaw
去标记mockjs
7.4 customRef —— 自定义Ref
作用:创建一个自定义的ref
,并对其依赖项跟踪和更新触发进行逻辑控制。
封装代码
实现防抖效果(useSumRef.ts
):
import {customRef } from "vue";
export default function(initValue:string,delay:number){
let 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}
}
八、Vue3新组件
8.1 Teleport —— 移动组件
什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
【基本用法】
有时我们可能会遇到这样的场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。
这类场景最常见的例子就是全屏的模态框。理想情况下,我们希望触发模态框的按钮和模态框本身是在同一个组件中,因为它们都与组件的开关状态有关。但这意味着该模态框将与按钮一起渲染在应用 DOM 结构里很深的地方。这会导致该模态框的 CSS 布局代码很难写。
<teleport to='body' >
<div class="modal" v-show="isShow">
<h2>我是一个弹窗</h2>
<p>我是弹窗中的一些内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
8.2 Suspense —— 异步组件
-
等待异步组件时渲染一些额外内容,让应用有更好的用户体验
-
使用步骤:
-
异步引入组件
-
使用
Suspense
包裹组件,并配置好default
与fallback
-
import { defineAsyncComponent,Suspense } from "vue";
const Child = defineAsyncComponent(()=>import('./Child.vue'))
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.......</h3>
</template>
</Suspense>
</div>
</template>
8.3 全局API转移到应用对象
1. app.component
在Vue.js中,app.component
用于全局注册组件,这样组件就可以在应用的任何模板中直接使用。通过这个方法注册的组件是全局可用的,不需要在每个组件文件中重复引入。
用法示例:
import { createApp } from 'vue';
import App from './App.vue';
import MyComponent from './components/MyComponent.vue';
const app = createApp(App);
app.component('MyComponent', MyComponent); // 全局注册组件
app.mount('#app');
2. app.config
(Vue中的配置方式)
虽然Vue.js中没有直接的app.config
属性用于配置(与AngularJS中的app.config
不同),但Vue允许通过Vue实例或全局配置对象来设置一些全局配置,如性能监控、开发模式等。
Vue全局配置示例(通过Vue实例的config
属性):
const app = createApp({});
app.config.productionTip = false; // 关闭生产提示
3. app.directive
在Vue.js中,app.directive
用于注册全局自定义指令。自定义指令用于对DOM元素进行底层操作,以封装一些常见的DOM操作逻辑。
用法示例:
app.directive('focus', {
mounted(el) {
el.focus();
}
});
4. app.mount
app.mount
是Vue.js中用于将Vue应用程序实例挂载到DOM元素上的方法。这是Vue应用程序启动的最后一步,它将Vue的根组件渲染到指定的DOM元素上。
用法示例:
const app = createApp(App);
app.mount('#app'); // 将Vue应用挂载到id为app的DOM元素上
5. app.unmount
app.unmount
是Vue.js中用于从DOM元素上卸载Vue应用程序实例的方法。这通常用于在单页面应用(SPA)中销毁并移除当前视图,以便加载新的视图。
用法示例(假设已有一个挂载的Vue实例):
app.unmount('#app'); // 从id为app的DOM元素上卸载Vue应用
6. app.use
app.use
在Vue.js中用于安装Vue插件。Vue插件可以扩展Vue的功能,如添加全局指令、混入(mixins)、过滤器等。
用法示例:
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
const app = createApp(App);
app.use(router); // 使用Vue Router插件
app.use(store); // 使用Vuex插件
app.mount('#app');
在Express等Node.js框架中,app.use
也用于注册中间件,但它与Vue.js中的用途完全不同。
8.4 其他非兼容性重大改变
-
过渡类名
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
。......Vue 3 迁移指南 (vuejs.org)