【Vue】Vue3的系统性学习

1、前言

之前在做一个springboot前后端分离项目的时候,前端使用的是Vue3。并不是说我会Vue3或者Vue2,而是Vue这东西是一个渐进式的框架,所有用啥可以学啥,随便学学就实现了一个功能了。但是在使用Vue3写前端的时候,遇到了非常多的麻烦,比如根本不会typescript,也不会使用最新的setup,同时语法还是使用老旧的Vue2,这就导致了虽然看起来我的前端功能完成了,但实际上其中的代码是一坨shit山的情况。

通过某个契机,准备进行Vue3的系统性学习。目前对于Vue的了解仅仅只有Vue2的一些语法格式,但是在本篇文章中,会进行Vue3的各种系统性学习。

2、Vue3介绍

由Vue2重构而来,使用MVVM架构编写。

  • View
  • ViewModel
  • Model

系统的介绍就看看Vue的官方文档吧

对比Vue2,Vue3有何改进?

  • 重写数据的双向绑定(Vue2使用Object.defineProperty(),Vue3使用Proxy
  • 优化了Vdom渲染
  • 允许有多个根节点
  • 智能导入,仅仅导入需要使用到的功能

3、开发环境

首先安装nodejs,前往官网下载,需求的版本 >= 12

再构建vite项目

为了快速访问npm中的资源,先安装一个cnpm或者给npm换源

npm install -g cnpm

到工作文件夹中输入如下代码进行构建

cnpm init vite@latest

选择构建vue项目

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

构建成功如下:

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

输入 cnpm run dev 进行启动项目

进入本地,构建成功

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

4、认识文件 & 文件夹

  • public文件夹用于存放静态资源,例如图片

  • src中是会被编译的源文件

    • assets虽然也是资源文件夹,但是其中的文件是会被编译的,例如图片可以编译成base64
    • components是用于存放公共组件,例如 页头 页尾
    • 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模式,也只能有一个

5、模板语法 & Vue指令

在Vue3中,模板语法是非常快速进行数据解析的一种方式,例如我在script中得到的变量需要渲染到dom中,使用{{ }}的方式就可以套入

{{ msg }}
let msg = "woodwhale"

同理,不仅仅支持字符串,还可以支持script语法,例如判断、api调用、计算等等

{{ msg.split("oo") }}
let msg = "woodwhale"

接下来就是Vue常用的指令,v- 开头的,都是vue的指令

  • v-text(用来显示文本)
  • v-html(用来显示富文本)
  • v-if(判断)
  • v-else-if
  • v-else
  • v-show(用来控制元素的显示和隐藏)
  • v-on(简写是@,表示给元素添加事件绑定,例如@click
  • v-bind(简写是:,用来绑定元素的属性Attr)
  • v-model(表示数据的双向绑定)
  • v-for(遍历)

6、Ref全家桶

到这里就是Vue3的用处非常多的Ref的出现了,这里介绍其全家桶套餐,分别是:

  • ref
  • Ref
  • isRef
  • shallowRef
  • triggerRef
  • customRef

分别由什么用呢?我们都来看一下:

6.1 ref与Ref

首先是refRef,ref是一个方法,而Ref是一个类型

ref,其用法就是将一个数据进行双向绑定,可以通过交互达到改变数据的作用

 
change
{{ msg }}
import { Ref, ref } from "vue"let msg: Ref = ref("hello world")const changeMsg = () => { msg.value = "changed msg"}

在如上的例子中,使用ref绑定一个msg的变量(数据类型是Ref类,同时绑定泛型为string),在绑定完之后,给一个button绑定一个点击方法,点击之后就会将msg.value进行改变,注意需要使用.value的属性对其数值进行改变

看效果:

6.2 isRef

从名字就看得出来,是一个判断方法,其实就是判断一个数据类型是否是Ref

 
change
{{ msg }}
import { isRef, Ref, ref } from "vue"let msg: Ref = ref("hello world")const changeMsg = () => { msg.value = "changed msg" console.log(isRef(msg))}

运行后会在浏览器控制行log出true

6.3 shallowRef

从名字看出来,shallo就是浅显的意思,也就是说,使用shalloRef是不会对深层属性进行双向绑定的

举个例子

 
change
{{ msg }}
import { shallowRef } from "vue"let msg = shallowRef({ "woodwhale": "sheepbotany" })const changeMsg = () => { console.log("尝试修改深层属性") msg.value.woodwhale = "another sheepbotany"}

我们点击按钮时不会改变msg.value.woodwhale

shalloRef只能对其value属性进行相应,所以上述代码可以改为

import { shallowRef } from "vue"let msg = shallowRef({ "woodwhale": "sheepbotany" })const changeMsg = () => { console.log("尝试修改深层属性") msg.value = { "woodwhale": "another sheepbotany" }}

修改后运行效果如下:

6.4 triggerRef

从名字也可以看出,叫做触发,那么到底时什么意思呢?其实算一种强制刷新ref的绑定

例如在上述的shallowRef中,我们无法对msg.value.woodwhale这种深层属性进行修改,如果想要修改成功,就需要使用triggerRef来调用强制刷新

import { shallowRef, triggerRef } from "vue"let msg = shallowRef({ "woodwhale": "sheepbotany" })const changeMsg = () => { console.log("尝试修改深层属性") msg.value.woodwhale = "another sheepbotany" triggerRef(msg)}

这样的也能达到实现修改shalloRef深层属性的效果

6.5 customRef

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 = newValtrigger() // 更新,刷新新数据 }} })}let msg = MyRef<string>("woodwhale")const changeMsg = () => { msg.value = "sheepbotany"}</script>

7、Reactive全家桶

7.1 reactive

最基本的reactiveref的性质类似,但是一般使用reactive来修饰非基本类型,例如:数组、类与对象

ref一般是用来进行修饰基本数据类型的,当然,非基本数据类型也可以修饰,但是其背后的底层逻辑还是会判断是否是基本数据类型,如果非基本数据类型,其会调用toReactive的方法,将其转换为Reactive类型

看一个基本的使用例子:

 
change
msg: {{ msg }}
obj: {{ obj }}
import { reactive } from 'vue';let msg = reactive([114,514])// reactive只能传入非基本类型的数据,例如数组、objectlet obj = reactive({ name:"woodwhale"})const changeMsg = () => { msg.push(...[1,2,3]) obj.name = "sheepbotany"}

注意这里如果要给msg进行增删改,需要使用函数类型的增删改实现,例如push方法等,不能给其重新赋值

7.2 shallowReactive

shallowRef类似,是浅层的数据绑定

在页面渲染完成之后,只能对浅层的数据进行修改

 
change
msg: {{ msg }}
import { shallowReactive } from 'vue';let msg = shallowReactive({ name: "woodwhale", deep: {deeper: { name: "sheepbotany"} }})const changeMsg = () => { msg.deep.deeper.name = "new sheepbotany"}

上述代码就无法在web端实时渲染,虽然值确实是改变了,但是web端的显示渲染没有进行改变

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

import { shallowReactive } from 'vue';let msg = shallowReactive({ name: "woodwhale", deep: {deeper: { name: "sheepbotany"} }})const changeMsg = () => { msg.name = "new woodwhale" msg.deep.deeper.name = "new sheepbotany"}

7.4 readonly

调用readonly方法进行一次数据的拷贝,copy的数据是无法进行修改的,只能读

import { reactive, readonly } from 'vue';let msg = reactive({ count: 1})let copy = readonly(msg)copy.count++ // readonly无法修改

8、to全家桶

分别对如下几种to的方法进行举例讲解

8.1 toRef

使用toRef可以将数据类型转为Ref类型

 
change
state: {{ state }}
import { toRef } from 'vue';const obj = { name: "woodwhale", sex: "man"}const state = toRef(obj, "name")const changeMsg = () => { state.value = "new woodwhale" console.log("原始对象 --> ", obj) console.log("to后对象 --> ", state) // 会对自身数据进行更新,也会对原始数据进行更新,但是页面的视图是不会变化的(因为原始obj不是相应式的,如果原始obj是相应式的就会变化)}

8.2 toRefs

将一个数据中的多个属性转为ref

 
change
obj: {{ obj }}
import { reactive, toRefs } from 'vue';const obj = reactive({ name: "woodwhale", sex: "man"})let {name,sex} = toRefs(obj)const changeMsg = () => { name.value = "sheepbotany" sex.value = "woman"}

8.3 toRaw

toRaw就是将相应式的数据转为原始的数据类型

import { reactive, toRaw, toRefs } from 'vue';const obj = reactive({ name: "woodwhale", sex: "man"})const raw = toRaw(obj)console.log("相应式 --> ", obj)console.log("非相应式 --> ", raw)

9、computed计算属性

所谓computed就是计算属性,也就是当依赖的属性发生变化的时候,才会触发其变更。如果依赖的值不发生改变,那么使用的就是缓存中的属性值。

 
{{name}}
import { computed,ref } from 'vue';let first = ref('woodwhale')let second = ref('sheepbotany')const name = computed(() => { return first.value + "----" + second.value}) // 只要first或者second进行了改变,name就会进行改变,这就是computed

我们尝试进行first和second的修改:

发现name也是一个相应式,只不过进行了ref的组合计算

当然computed的使用方法还可以是对象的方式,使用getset方法:

 
{{ name }}
import { computed, ref } from 'vue';let first = ref('woodwhale')let second = ref('sheepbotany')const name = computed({ get() {return first.value + "----" + second.value // 3. 触发get方法,页面获取真实的name }, set(param) {second.value = param // 2. 触发set方法 }})setTimeout(() => { name.value = "new sheepbatany"}, 1000)// 1. 三秒后给 name 赋值

10、watch监听器

watch是vue3中的一个方法,可以监听数据的改变

10.1 ref浅监听

watch默认是浅监听,也就是如果一个ref对象有深层属性,是无法通过浅层监听而监听到的

 
msg --> {{ msg }}
old --> {{ o }}
new --> {{ n }}
import { ref, watch } from 'vue';let msg = ref('woodwhale')let n = ref("")let o = ref("")// 监听器,监听msgwatch(msg, (newVal, oldVal) => { n.value = newVal o.value = oldVal})

我们可以观察到,每次改变msg的值,都会调用watch中的方法

10.2 ref深监听

watch方法的第三个参数中设置deep:true就可以进行深层监听,这样就可以监听ref对象中的深层属性

 
msg --> {{ msg.d.dd.ddd }}
old --> {{ o }}
new --> {{ n }}
import { ref, watch } from 'vue';let msg = ref({ d: {dd: { ddd: "woodwhale"} }})let n = ref("")let o = ref("")// 监听器,监听msg,使用deep功能,进行深度监听watch(msg, (newVal, oldVal) => { n.value = newVal.d.dd.ddd o.value = oldVal.d.dd.ddd}, { deep: true })

使用深层监听有一个缺点,那就是newValoldVal是一样的

10.3 默认执行

watch方法放在setup中是默认第一次不会执行的,也就是说,如果不改变监听的对象的属性,那么watch中的方法是不会进行调用的。

如果想让第一次执行就进行调用,那就需要使用到immediate:true

 
msg --> {{ msg.d.dd.ddd }}
old --> {{ o }}
new --> {{ n }}
import { ref, watch } from 'vue';let msg = ref({ d: {dd: { ddd: "woodwhale"} }})let n = ref()let o = ref()// 监听器,监听msg,使用deep功能,进行深度监听watch(msg, (newVal, oldVal) => { n.value = newVal.d.dd.ddd if (oldVal === undefined) {o.value = "页面加载会调用" } else {o.value = oldVal.d.dd.ddd }}, { deep: true, immediate: true})

这里的watch中的方法就是页面已加载就进行调用了

10.4 reactive监听

对于reactivewatch监听,无论是否加入deep:true,它都是深层监听,因为reactive定义的对象本来就是面向属性层次的。

加入一个reactive对象中有多个属性,但是我们仅仅想监听其中的某一个属性,可以将watch的第一个参数写成函数的形式,然后返回reactive对象的属性

 
msg --> {{ msg }}
import { reactive, watch } from 'vue';let msg = reactive({ name: "woodwhale", name2: "sheepbotany"})// 监听器,监听msg,使用deep功能,进行深度监听watch(() => msg.name, (newVal, oldVal) => { console.log("new", newVal) console.log("old", oldVal)})

这里我只对name属性进行了监听,而没有对name2进行监听

11、watchEffect监听器

watchEffect就是可以监听多个属性改变的监听器,其中存在的属性都会被监听

11.1 基本方法

 
msg --> {{ msg }}
msg2 --> {{ msg2 }}
import { ref, watchEffect } from 'vue';let msg = ref("msg1")let msg2 = ref("msg2")// 首次一定会调用watchEffect(() => { console.log("msg --> ", msg.value) console.log("msg2 --> ", msg2.value)})

11.2 预处理

如果我们需要在监听之前进行预处理呢?在匿名方法中传入一个参数就行了

 
msg --> {{ msg }}
msg2 --> {{ msg2 }}
import { ref, watchEffect } from 'vue';let msg = ref("msg1")let msg2 = ref("msg2")// 首次一定会调用watchEffect((beforeMethod) => { console.log("msg --> ", msg.value) console.log("msg2 --> ", msg2.value) // 在调用上面两个log之前会调用beforeMethod中的匿名函数,页面首次调用除外 beforeMethod(() => {console.log("before done!") })})

11.3 关闭监听

如果我们想要关闭watchEffect的监听呢,也很简单调用stop()方法就可以了

 
msg --> {{ msg }}
msg2 --> {{ msg2 }}
stopimport { ref, watchEffect } from 'vue';let msg = ref("msg1")let msg2 = ref("msg2")// 首次一定会调用const watchVal = watchEffect((beforeMethod) => { console.log("msg --> ", msg.value) console.log("msg2 --> ", msg2.value) // 在调用上面两个log之前会调用beforeMethod中的匿名函数,页面首次调用除外 beforeMethod(() => {console.log("before done!") })})// 关闭监听const stopWatch = () => watchVal()

12、Vue3生命周期

在介绍了上述这么多的Vue3的语法糖,可以引入vue的声明周期函数来进行讲解了。

vue3中有一个setup的状态,对应vue2中的beforeCreatecreated

这里先介绍六种生命周期函数:

  • onBeforeMount
  • onMounted
  • onBeforeUpdate
  • onUpdated
  • onBeforeUnmount
  • onUnmounted
 
{{ count }} 《-- 点击改变
import { onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from 'vue';const count = ref(0)const countUpdate = () => { count.value++}console.log("setup")onBeforeMount(() => { console.log("创建之前 --> onBeforeMount")})onMounted(() => { console.log("创建完成 --> onMounted")})onBeforeUpdate(() => { console.log("更新之前 --> onBeforeUpdate")})onUpdated(() => { console.log("更新完成 --> onUpdated")})onBeforeUnmount(() => { console.log("卸载之前 --> onBeforeUnmount")})onUnmounted(() => { console.log("卸载完成 --> onUnmounted")})

一张图看如上的生命周期

这里的卸载按钮写在App.vue中

点击 {{ state }} baseViewimport { ref } from 'vue';import BaseViewVue from './layout/BaseView.vue'let flag = ref(true)let state = ref("销毁")const des = () => {if (flag.value) {flag.value = falsestate.value = "挂载"} else {flag.value = truestate.value = "销毁"}}

13、父子组件传递参数

首先先编写一个父子组件


BaseView中引入ContentHeaderMenu三块内容,其代码如下

 
import Content from './Content/index.vue'import Header from './Header/index.vue'import Menu from './Menu/index.vue' .layout { display: flex;}

13.1 父传子

将父组件的内容传递给子组件也很简单,例如在BaseViewMenu传递

其中menuStr是普通类型的字符串,menuList是数组类型,需要使用v-bind,简写成:的形式

我们在Menu中进行接收父组件传递的两个参数

菜单
{{ menuStr }}
{{ menuList }}
// 从父控件里传来的参数type props = {menuStr: stringmenuList: number[]}defineProps()

setupts的情况下,使用defineProps接收传入的参数,应以一个type类型的props,其中存放的就是存入参数的申明

显示效果如下:

当然,如果我们在子组件中想定义可有可无的参数,如何实现呢?

其实只要在type中加一个" />type props = {menuStr?: string,menuList?: number[]}

但是这样就没有默认值了,如果需要默认值,在ts的环境下,使用withDefaults方法就可以了

子组件Menu的代码

菜单
{{ menuStr }}
{{ menuList }}
type props = {menuStr?: string,menuList?: number[]}withDefaults(defineProps(), {menuStr: "默认str",menuList: () => [1, 2, 3, 4]})

父组件BaseView的代码

 
import Content from './Content/index.vue'import Header from './Header/index.vue'import Menu from './Menu/index.vue' .layout { display: flex;}

13.2 子传父(事件)

使用defineEmits方法可以将子组件的事件传递给父组件

子组件Menu代码

菜单
{{ menuStr }}
{{ menuList }}
派发
import { ref } from 'vue';// 从父控件里传来的参数type props = {menuStr: stringmenuList: number[]}defineProps()const list = ref("my list")const emits = defineEmits(["my-click"])const clickTap = () => {emits("my-click",list)// 派发 my-click方法}

可以看到暴露了一个my-click的事件

在父组件中BaseView的代码

 
import { Ref } from 'vue';import Content from './Content/index.vue'import Header from './Header/index.vue'import Menu from './Menu/index.vue'const getList = (list: Ref) => { console.log("子组件传过来的ref --> " + list.value)} .layout { display: flex;}

Menu标签里写入@my-click定义好的事件,就可以进行触发,而在之前是传入了list的Ref对象,那么在父组件中就可以接收这个对象

13.3 子传父(属性)

通过defineExpose方法可以将自己这个组件的属性给暴露出来

我们先在Menu子组件中暴露一些属性出来

菜单
{{ menuStr }}
{{ menuList }}
import { ref } from 'vue';let menuStr = ref("menuStr")let menuList = ref(["item1", "item2"])// 将自己的属性暴露出去defineExpose({menuStr,menuList})

然后在父组件BaseView中读取

 
读取Menu的ref对象
import { ref } from 'vue';import Content from './Content/index.vue'import Header from './Header/index.vue'import Menu from './Menu/index.vue'let menuRef = ref({ menuStr: "", menuList: []})const logMenu = () => { console.log(menuRef.value.menuStr) console.log(menuRef.value.menuList)} .layout { display: flex;}

注意这里需要给Menu标签注入一个ref,即**

**

然后再ts中也要定义一个相对应的menuRef

14、动态组件

在vue3中,使用标签来完成动态组件的使用

 
<!-- -->
import Content from './Content/index.vue'import Header from './Header/index.vue'import Menu from './Menu/index.vue' .layout { display: flex;}

这里使用component的作用和直接使用

的效果是一样的,但是动态组件之所以叫动态组件,是因为可以改变is的属性,从而做到切换组件的作用

15、插槽

使用v-slot或者简写成#来使用

现在Menu中申明两个插槽,第一个有name,第二个没有

菜单

然后再BaseView中的组件中插入插槽

 
上部的插槽
下部的插槽
import Content from './Content/index.vue'import Header from './Header/index.vue'import Menu from './Menu/index.vue' .layout { display: flex;}

注意,默认插槽使用#default,有名字的插槽例如这里的#top。效果如下:

如果我想让申明插槽的子组件将一些属性传递给父组件,如何完成?使用v-bind即可

子组件代码,定义了:data="item"

菜单
type myMap = {key: string,value: string}let map: myMap[] = [{key: "name1",value: "woodwhale"},{key: "name2",value: "sheepbotany"}]

父组件接收,使用#top="{ data }"接收data

 
{{ data }}
下部的插槽

如果我们想使用动态插槽呢?使用变量即可

 
{{ data }}
下部的插槽
import Content from './Content/index.vue'import Header from './Header/index.vue'import Menu from './Menu/index.vue'let slotVal = { first: "top", second: "default"}

使用#[slotVal.first]#[slotVal.second]来动态的申明插槽

16、异步组件

为了模拟后端接口请求,先在public文件夹下创建一个data.json文件

[{"name": "woodwhale"},{"name": "sheepbotany"}]

然后,在compontents文件夹下创建一个组件文件夹,我这里叫做AsyCom

AsyCom中,创建一个index.vue和一个server.ts

server.ts中,导出一个异步请求的方法axios

type NameList = {name: string}export const axios = (url: string): Promise<NameList[]> => {return new Promise((resolve) => {let xhr = new XMLHttpRequest()xhr.open("GET", url)xhr.send(null)xhr.onreadystatechange = () => {if (xhr.status === 200 && xhr.readyState === 4) {setTimeout(() => {resolve(JSON.parse(xhr.responseText)) // 返回}, 2000)}}})}

这里模拟了2s的延迟

之后在index.vue中渲染json获取的name

{{ item.name }}
import { axios } from './server';const data = await axios("./data.json")

现在问题就是,如何引入这样的一个异步组件?

我们回到Menu组件中,通过defineAsyncComponent() + import()的方法引入异步组件

import { defineAsyncComponent } from "vue";const AsyCom = defineAsyncComponent(() => import("../../components/AsyCom/index.vue"))

引入完了还需要进行渲染,需要使用到标签,其中有两个插槽

  • default(接收到异步消息后渲染)
  • fallback(请求消息中的loading)

所以Menu的代码为

菜单
loading...import { defineAsyncComponent } from "vue";const AsyCom = defineAsyncComponent(() => import("../../components/AsyCom/index.vue"))

最终的效果如下:

17、Teleport

Teleport是vue3的新特性,叫做传送组件

它的功能就是为了防止样式的冲突的情况下可以进行组件引入

我们随意在一个组件中进行插入,使用Teleport插入不需要担心css渲染问题

菜单
我被插入到body中了
.insert {font-size: 20px;}

使用to="body",这样其中的内容就被插入到了body中

18、keep-active

keep-active组件是为了保存缓存而设置的

举个例子,假设我现在有loginregister两个组件,需要通过点击一个button进行组件的切换

如果我们使用动态组件的形式,那么每一次输入的数据会被重新渲染(消失),所以为了保存缓存,需要使用keep-active

首先是login的代码

用户名:
密码:
提交登录import { reactive } from "vue"type LoginForm = {username: stringpassword: string}let form = reactive({username: "",password: ""})const sub = () => {console.log(form)}

然后是register的代码

用户名:
密码:
验证码:
提交注册import { reactive } from "vue"type RegForm = {username: stringpassword: stringcode:string}let form = reactive({username: "",password: "",code:""})const sub = () => {console.log(form)}

最后在Menu组件上放置两个子组件

菜单
切换import { Ref, ref } from "vue";import Login from "../../components/Login/index.vue"import Register from "../../components/Register/index.vue"let flag: Ref = ref(true)

效果如下:

可以看到两个组件的状态被保存了

当然,keep-active是有参数的,比如include、exclude,就是包含或者不包含某个组件

例如我这里只需要保存Login组件的状态

首先需要去Login组件中申明一个name属性

export default {name: "Login"}

然后在Menu中的keep-active设置include = "Login"

菜单
切换

当然exclude同理,这里就不演示了。

19、依赖注入

依赖注入这里值的是provideinject

在深度嵌套的关系下,如果仅仅使用父子组件传参,是非常麻烦的。

这里引入的依赖注入技术就是为了解决这样的问题而实现的。

在父组件中提供provide就可以在任何子组件中使用inject获取

举个例子,首先在App.vue中引入A这个子组件

我是root
import { provide } from "vue";import A from "./components/A.vue"provide("str","这是root中注入的字符串").root {width: 250px;height: 250px;background-color: red;}

在A中引入B

我是A
import B from "./B.vue".a {width: 200px;height: 200px;background-color: yellow;}

最后可以在B中通过inject获取root中的str

我是B
{{str}}
import { inject } from 'vue';let str = inject("str").b {width: 150px;height: 150px;background-color: blue;}

页面效果如下:

但是我们上述提供的是非相应式的str

如果需要使用相应式,就是用ref或者reactive

两种方法:

  • 强转
  • 兜底
// 父组件provide("str",ref("这是root中注入的字符串"))// 1.子组件强转let str = inject("str") as Refstr.value = "114514"// 2.兜底逻辑let str = inject("str",ref(""))str.value = "1919810"

20、v-model

在Vue3中v-model是一个破坏性更新(相对于Vue2)

改变如下:

  • prop –> modelValue
  • event –> update:modelVale
  • 支持多个v-model
  • 支持自定义修饰符

因为v-model是双向绑定的,所以可以在子组件中更改父组件的值

父组件代码中,给子组件A一个v-model="flag"

我是rootflag --> {{flag}}
import { ref } from "vue";import A from "./components/A.vue"let flag = ref(true).root {width: 250px;height: 250px;background-color: red;}

子组件A代码,使用defineProps接收modelValue默认绑定值,同时通过defineEmits派发事件update:modelValue,将modelValue改为false

我是A close
type props = { modelValue: boolean }defineProps()const emit = defineEmits(["update:modelValue"])const close = () => {emit("update:modelValue",false) // 将modelValue改为false}.a {width: 200px;height: 200px;background-color: yellow;}

效果如下,可以发现是父子之间双向绑定

还可以使用自定义的v-model

// 父组件// 父组件 setup tslet flag = ref(true)let woodwhale = ref("sheepbotany")// 子组件 setup tstype props = { modelValue: booleanwoodwhale: string }defineProps()

21、全局变量

在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进行申明