之前在做一个springboot前后端分离项目的时候,前端使用的是Vue3。并不是说我会Vue3或者Vue2,而是Vue这东西是一个渐进式的框架,所有用啥可以学啥,随便学学就实现了一个功能了。但是在使用Vue3写前端的时候,遇到了非常多的麻烦,比如根本不会typescript,也不会使用最新的setup,同时语法还是使用老旧的Vue2,这就导致了虽然看起来我的前端功能完成了,但实际上其中的代码是一坨shit山的情况。
通过某个契机,准备进行Vue3的系统性学习。目前对于Vue的了解仅仅只有Vue2的一些语法格式,但是在本篇文章中,会进行Vue3的各种系统性学习。
由Vue2重构而来,使用MVVM架构编写。
系统的介绍就看看Vue的官方文档吧
对比Vue2,Vue3有何改进?
Object.defineProperty(),Vue3使用Proxy)首先安装nodejs,前往官网下载,需求的版本 >= 12
再构建vite项目
为了快速访问npm中的资源,先安装一个cnpm或者给npm换源
npm install -g cnpm
到工作文件夹中输入如下代码进行构建
cnpm init vite@latest
选择构建vue项目

我这里选择的是vue-ts版本

构建成功如下:

进入到创建好的init文件夹,进行install

输入 cnpm run dev 进行启动项目

进入本地,构建成功

如果使用vscode进行开发,那么有很多的插件可以使用

public文件夹用于存放静态资源,例如图片
src中是会被编译的源文件
assets虽然也是资源文件夹,但是其中的文件是会被编译的,例如图片可以编译成base64components是用于存放公共组件,例如 页头 页尾App.vue文件是应用于全局的vue文件main.ts文件也是公共全局的ts文件index.html是首页文件,比较重要
package.json是依赖管理配置
tsconfig.json是typescript的配置文件
vite.config.ts是vite的配置文件
在一个vue文件中,由三部分组成template、script、style
template在一个vue文件中只能有一个,scripte如果是setup模式,也只能有一个
在Vue3中,模板语法是非常快速进行数据解析的一种方式,例如我在script中得到的变量需要渲染到dom中,使用{{ }}的方式就可以套入
{{ msg }}
同理,不仅仅支持字符串,还可以支持script语法,例如判断、api调用、计算等等
{{ msg.split("oo") }}
接下来就是Vue常用的指令,v- 开头的,都是vue的指令
v-if(判断)v-else-ifv-elsev-on(简写是@,表示给元素添加事件绑定,例如@click)v-bind(简写是:,用来绑定元素的属性Attr)v-model(表示数据的双向绑定)v-for(遍历)到这里就是Vue3的用处非常多的Ref的出现了,这里介绍其全家桶套餐,分别是:
分别由什么用呢?我们都来看一下:
首先是ref和Ref,ref是一个方法,而Ref是一个类型
ref,其用法就是将一个数据进行双向绑定,可以通过交互达到改变数据的作用
change
{{ msg }}
在如上的例子中,使用ref绑定一个msg的变量(数据类型是Ref类,同时绑定泛型为string),在绑定完之后,给一个button绑定一个点击方法,点击之后就会将msg.value进行改变,注意需要使用.value的属性对其数值进行改变
看效果:

从名字就看得出来,是一个判断方法,其实就是判断一个数据类型是否是Ref
change
{{ msg }}
运行后会在浏览器控制行log出true

从名字看出来,shallo就是浅显的意思,也就是说,使用shalloRef是不会对深层属性进行双向绑定的
举个例子
change
{{ msg }}
我们点击按钮时不会改变msg.value.woodwhale的

shalloRef只能对其value属性进行相应,所以上述代码可以改为
修改后运行效果如下:

从名字也可以看出,叫做触发,那么到底时什么意思呢?其实算一种强制刷新ref的绑定
例如在上述的shallowRef中,我们无法对msg.value.woodwhale这种深层属性进行修改,如果想要修改成功,就需要使用triggerRef来调用强制刷新
这样的也能达到实现修改shalloRef深层属性的效果
customRef是一个可以自定义相应式的ref,用法如下。如下的写法其实就是ref的原理
<script setup lang='ts'>
import { customRef } from "vue"
function MyRef<T>(value:T) {
return customRef((trank,trigger) => {
return {
get() {
trank() // 跟踪获取数据
return value
},
set(newVal:T) {
value = newVal
trigger() // 更新,刷新新数据
}
}
})
}
let msg = MyRef<string>("woodwhale")
const changeMsg = () => {
msg.value = "sheepbotany"
}
</script>
最基本的reactive和ref的性质类似,但是一般使用reactive来修饰非基本类型,例如:数组、类与对象
而ref一般是用来进行修饰基本数据类型的,当然,非基本数据类型也可以修饰,但是其背后的底层逻辑还是会判断是否是基本数据类型,如果非基本数据类型,其会调用toReactive的方法,将其转换为Reactive类型
看一个基本的使用例子:
change
msg: {{ msg }}
obj: {{ obj }}
注意这里如果要给msg进行增删改,需要使用函数类型的增删改实现,例如push方法等,不能给其重新赋值

和shallowRef类似,是浅层的数据绑定
在页面渲染完成之后,只能对浅层的数据进行修改
change
msg: {{ msg }}
上述代码就无法在web端实时渲染,虽然值确实是改变了,但是web端的显示渲染没有进行改变

但是如果在改变浅层数据的时候,一并改变深层数据,两者都会进行web端的渲染更新

调用readonly方法进行一次数据的拷贝,copy的数据是无法进行修改的,只能读
分别对如下几种to的方法进行举例讲解
使用toRef可以将数据类型转为Ref类型
change
state: {{ state }}
将一个数据中的多个属性转为ref
change
obj: {{ obj }}
toRaw就是将相应式的数据转为原始的数据类型

所谓computed就是计算属性,也就是当依赖的属性发生变化的时候,才会触发其变更。如果依赖的值不发生改变,那么使用的就是缓存中的属性值。
{{name}}
我们尝试进行first和second的修改:

发现name也是一个相应式,只不过进行了ref的组合计算
当然computed的使用方法还可以是对象的方式,使用get和set方法:
{{ name }}

watch是vue3中的一个方法,可以监听数据的改变
watch默认是浅监听,也就是如果一个ref对象有深层属性,是无法通过浅层监听而监听到的
msg --> {{ msg }}
old --> {{ o }}
new --> {{ n }}

我们可以观察到,每次改变msg的值,都会调用watch中的方法
在watch方法的第三个参数中设置deep:true就可以进行深层监听,这样就可以监听ref对象中的深层属性
msg --> {{ msg.d.dd.ddd }}
old --> {{ o }}
new --> {{ n }}

使用深层监听有一个缺点,那就是newVal和oldVal是一样的
watch方法放在setup中是默认第一次不会执行的,也就是说,如果不改变监听的对象的属性,那么watch中的方法是不会进行调用的。
如果想让第一次执行就进行调用,那就需要使用到immediate:true了
msg --> {{ msg.d.dd.ddd }}
old --> {{ o }}
new --> {{ n }}
这里的watch中的方法就是页面已加载就进行调用了

对于reactive的watch监听,无论是否加入deep:true,它都是深层监听,因为reactive定义的对象本来就是面向属性层次的。
加入一个reactive对象中有多个属性,但是我们仅仅想监听其中的某一个属性,可以将watch的第一个参数写成函数的形式,然后返回reactive对象的属性
msg --> {{ msg }}

这里我只对name属性进行了监听,而没有对name2进行监听
watchEffect就是可以监听多个属性改变的监听器,其中存在的属性都会被监听
msg --> {{ msg }}
msg2 --> {{ msg2 }}
如果我们需要在监听之前进行预处理呢?在匿名方法中传入一个参数就行了
msg --> {{ msg }}
msg2 --> {{ msg2 }}
如果我们想要关闭watchEffect的监听呢,也很简单调用stop()方法就可以了
msg --> {{ msg }}
msg2 --> {{ msg2 }}
stop
在介绍了上述这么多的Vue3的语法糖,可以引入vue的声明周期函数来进行讲解了。
vue3中有一个setup的状态,对应vue2中的beforeCreate和created
这里先介绍六种生命周期函数:
{{ count }} 《-- 点击改变
一张图看如上的生命周期

这里的卸载按钮写在App.vue中
点击 {{ state }} baseView
首先先编写一个父子组件

BaseView中引入Content、Header、Menu三块内容,其代码如下
将父组件的内容传递给子组件也很简单,例如在BaseView给Menu传递
其中menuStr是普通类型的字符串,menuList是数组类型,需要使用v-bind,简写成:的形式
我们在Menu中进行接收父组件传递的两个参数
菜单
{{ menuStr }}
{{ menuList }}
在setup和ts的情况下,使用defineProps接收传入的参数,应以一个type类型的props,其中存放的就是存入参数的申明
显示效果如下:

当然,如果我们在子组件中想定义可有可无的参数,如何实现呢?
其实只要在type中加一个?表示可以省略就行了
type props = {
menuStr?: string,
menuList?: number[]
}
但是这样就没有默认值了,如果需要默认值,在ts的环境下,使用withDefaults方法就可以了
子组件Menu的代码
菜单
{{ menuStr }}
{{ menuList }}
父组件BaseView的代码

使用defineEmits方法可以将子组件的事件传递给父组件
子组件Menu代码
菜单
{{ menuStr }}
{{ menuList }}
派发
可以看到暴露了一个my-click的事件
在父组件中BaseView的代码
在Menu标签里写入@my-click定义好的事件,就可以进行触发,而在之前是传入了list的Ref对象,那么在父组件中就可以接收这个对象

通过defineExpose方法可以将自己这个组件的属性给暴露出来
我们先在Menu子组件中暴露一些属性出来
菜单
{{ menuStr }}
{{ menuList }}
然后在父组件BaseView中读取
读取Menu的ref对象
注意这里需要给Menu标签注入一个ref,即**
然后再ts中也要定义一个相对应的menuRef
在vue3中,使用标签来完成动态组件的使用
这里使用component的作用和直接使用的效果是一样的,但是动态组件之所以叫动态组件,是因为可以改变is的属性,从而做到切换组件的作用
使用v-slot或者简写成#来使用
现在Menu中申明两个插槽,第一个有name,第二个没有
菜单
然后再BaseView中的组件中插入插槽
上部的插槽
下部的插槽
注意,默认插槽使用#default,有名字的插槽例如这里的#top。效果如下:

如果我想让申明插槽的子组件将一些属性传递给父组件,如何完成?使用v-bind即可
子组件代码,定义了:data="item"
菜单
父组件接收,使用#top="{ data }"接收data
{{ data }}
下部的插槽

如果我们想使用动态插槽呢?使用变量即可
{{ data }}
下部的插槽
现在问题就是,如何引入这样的一个异步组件?
我们回到Menu组件中,通过defineAsyncComponent() + import()的方法引入异步组件
引入完了还需要进行渲染,需要使用到标签,其中有两个插槽
所以Menu的代码为
菜单
loading...
最终的效果如下:

Teleport是vue3的新特性,叫做传送组件
它的功能就是为了防止样式的冲突的情况下可以进行组件引入
我们随意在一个组件中进行插入,使用Teleport插入不需要担心css渲染问题
菜单
我被插入到body中了
使用to="body",这样其中的内容就被插入到了body中

keep-active组件是为了保存缓存而设置的
举个例子,假设我现在有login和register两个组件,需要通过点击一个button进行组件的切换
如果我们使用动态组件的形式,那么每一次输入的数据会被重新渲染(消失),所以为了保存缓存,需要使用keep-active
首先是login的代码
用户名:
密码:
提交登录
然后是register的代码
用户名:
密码:
验证码:
提交注册
最后在Menu组件上放置两个子组件
菜单
切换
效果如下:

可以看到两个组件的状态被保存了
当然,keep-active是有参数的,比如include、exclude,就是包含或者不包含某个组件
例如我这里只需要保存Login组件的状态
首先需要去Login组件中申明一个name属性
然后在Menu中的keep-active设置include = "Login"
菜单
切换
当然exclude同理,这里就不演示了。
依赖注入这里值的是provide和inject
在深度嵌套的关系下,如果仅仅使用父子组件传参,是非常麻烦的。
这里引入的依赖注入技术就是为了解决这样的问题而实现的。
在父组件中提供provide就可以在任何子组件中使用inject获取
举个例子,首先在App.vue中引入A这个子组件
在A中引入B
我是A
最后可以在B中通过inject获取root中的str
我是B
{{str}}
页面效果如下:

但是我们上述提供的是非相应式的str
如果需要使用相应式,就是用ref或者reactive
两种方法:
// 父组件
provide("str",ref("这是root中注入的字符串"))
// 1.子组件强转
let str = inject("str") as Ref
str.value = "114514"
// 2.兜底逻辑
let str = inject("str",ref(""))
str.value = "1919810"
在Vue3中v-model是一个破坏性更新(相对于Vue2)
改变如下:
因为v-model是双向绑定的,所以可以在子组件中更改父组件的值
父组件代码中,给子组件A一个v-model="flag"
子组件A代码,使用defineProps接收modelValue默认绑定值,同时通过defineEmits派发事件update:modelValue,将modelValue改为false
我是A
close
效果如下,可以发现是父子之间双向绑定

还可以使用自定义的v-model
// 父组件
// 父组件 setup ts
let flag = ref(true)
let woodwhale = ref("sheepbotany")
// 子组件 setup ts
type props = {
modelValue: boolean
woodwhale: string
}
defineProps()
在Vue3中,使用app.config.globalProperties来定义全局的变量和方法。
因为Vue3删除了Vue2中的filters,所以我们可以在全局属性中自己写一个filters
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import "./assets/reset.less"
const app = createApp(App)
type Filter = {
format: <T>(str: T) => string
}
declare module "@vue/runtime-core" {
export interface ComponentCustomProperties {
$filters: Filter,
$val: string
}
}
// 全局变量
app.config.globalProperties.$val = "114514"
// 过滤器
app.config.globalProperties.$filters = {
format<T>(str: T): string {
return "formate之后的str --> " + str
}
}
app.use(ElementPlus)
app.mount('#app')
因为我这里运行的是ts版本的vue,所以为了不让编译器报错,需要使用declare进行申明