【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
虽然也是资源文件夹,但是其中的文件是会被编译的,例如图片可以编译成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模式,也只能有一个
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
首先是ref
和Ref
,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
最基本的reactive
和ref
的性质类似,但是一般使用reactive
来修饰非基本类型,例如:数组、类与对象
而ref
一般是用来进行修饰基本数据类型的,当然,非基本数据类型也可以修饰,但是其背后的底层逻辑还是会判断是否是基本数据类型,如果非基本数据类型,其会调用toReactive
的方法,将其转换为Reactive
类型
看一个基本的使用例子:
changemsg: {{ 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
类似,是浅层的数据绑定
在页面渲染完成之后,只能对浅层的数据进行修改
changemsg: {{ 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
类型
changestate: {{ 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
changeobj: {{ 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
的使用方法还可以是对象的方式,使用get
和set
方法:
{{ 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 })
使用深层监听有一个缺点,那就是newVal
和oldVal
是一样的
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监听
对于reactive
的watch
监听,无论是否加入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中的beforeCreate
和created
这里先介绍六种生命周期函数:
- 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
中引入Content
、Header
、Menu
三块内容,其代码如下
import Content from './Content/index.vue'import Header from './Header/index.vue'import Menu from './Menu/index.vue' .layout { display: flex;}
13.1 父传子
将父组件的内容传递给子组件也很简单,例如在BaseView
给Menu
传递
其中menuStr
是普通类型的字符串,menuList
是数组类型,需要使用v-bind
,简写成:
的形式
我们在Menu
中进行接收父组件传递的两个参数
菜单{{ menuStr }}{{ menuList }}// 从父控件里传来的参数type props = {menuStr: stringmenuList: number[]}defineProps()
在setup
和ts
的情况下,使用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
组件是为了保存缓存而设置的
举个例子,假设我现在有login
和register
两个组件,需要通过点击一个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、依赖注入
依赖注入这里值的是provide
和inject
在深度嵌套的关系下,如果仅仅使用父子组件传参,是非常麻烦的。
这里引入的依赖注入技术就是为了解决这样的问题而实现的。
在父组件中提供provide
就可以在任何子组件中使用inject
获取
举个例子,首先在App.vue中引入A这个子组件
我是rootimport { provide } from "vue";import A from "./components/A.vue"provide("str","这是root中注入的字符串").root {width: 250px;height: 250px;background-color: red;}
在A中引入B
我是Aimport 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 closetype 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
进行申明