本文介绍了 Vue3 组件之间的 8 种通信方式
随着最新vue版本升级带来的变化,本文还会持续更新~
- props
- emit
- v-model(比较 vue2/vue3 的不同写法,以及3.3+新增的 defineModel)
- provide / inject
- refs
- eventBus(事件总线通信)
- pinia
- slot(插槽)
这里的 Demo 都使用 Composition API 和 语法糖 <script setup>(vue3.2开始支持),这样写起来自由又方便,不用每次 return
props
用于父组件向子组件传值,是由上而下的单向数据流。官网的图示:
父组件:
<template>
<div>
<button @click="handleClick">点我,打个招呼</button>
<ChildComp :said="words" />
</div>
</template>
<script setup name="ParentComp">
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
const words = ref('')
const handleClick = () => {
words.value = '嘿!朋友'
}
</script>
子组件:
<template>
<p>{{said}}</p>
</template>
<script setup name="ChildComp">
import { defineProps } from 'vue'
const props = defineProps({
said: String
})
</script>
对于数组和对象类型的 prop,由于 js 中对于他们是通过引用传入的,如果误操作在组件内修改了这些 prop 会影响父组件的状态而引发意外的 bug,而 Vue 是不会提示的,所以千万别这么干
emit
和 props 刚好相反,通过 emit 向父组件触发自定义事件 可以由下而上从子组件向父组件传值
子组件:
<template>
<button @click="send">点我</button>
</template>
<script setup name="ChildComp">
import { defineEmits } from 'vue'
const emit = defineEmits(['change'])
const send = () => {
const times = `${Date.now()}`
emit('change', times)
}
</script>
父组件:
<template>
<div>
<ChildComp @change="handleChange" />
<div>当前时间:{{dateStr}}</div>
</div>
</template>
<script setup name="ParentComp">
import { ref } from 'vue'
import dayjs from 'dayjs'
import ChildComp from './ChildComp.vue'
const dateStr = ref('')
const handleChange = (times) => {
dateStr.value = dayjs(times).format('YYYY-MM-DD HH:mm:ss')
}
</script>
v-model
1.先回顾下 Vue2 中 v-model 的写法
<ChildComp :value="siteList" @input="siteList = $event" />
<!-- 以下是简写: -->
<ChildComp v-model="siteList" />
在组件上使用 v-model,相当于绑定属性 value 并触发 input 事件
默认是 value,如果想改变属性名,可以使用 model 选项绑定自定义属性名和事件名(v2.2.0+)
子组件 ChildComp:
export default {
model: {
prop: 'siteList', // v-model绑定的属性
event: 'change' // v-model绑定的自定义事件名
},
props: {
siteList: {
type: Array,
// 数据结构:[{ id: '01', checked: false }, ...]
default: () => ([])
}
},
methods: {
toggleSiteItem(id) {
if (!this.siteList.length) return
const _siteList = this.siteList.map((item) => {
if (item.id === id) {
return { ...item, checked: !item.checked }
}
return item
})
// 触发v-model绑定的自定义事件'change',并传值
this.$emit('change', _siteList)
}
}
}
在 父组件 中使用:
<template>
<button @click="checkAllSiteList">全选</button>
<ChildComp v-model="siteList" />
</template>
<script>
import ChildComp from './ChildComp'
export default {
components: { ChildComp },
data() {
return {
siteList: [
{ id: '01', checked: false },
{ id: '02', checked: false },
]
}
},
methods: {
// 全选
checkAllSiteList() {
this.siteList = this.siteList.map((item) => {
if (!item.checked) return { ...item, checked: true }
return item
})
}
}
}
</script>
这样在子组件和父组件中都能修改 siteList 了
2. Vue3 的 v-model:
默认使用 modelValue 替代了 2.× 的 value,update:modelValue 替代了 2.× 的 input
<ChildComp v-model="pageTitle" />
<!-- 是以下的简写: -->
<ChildComp :modelValue="pageTitle" @update:modelValue="pageTitle = $event"/>
可以将 modelValue 换成自定义属性名,这里换成 title
<ChildComp v-model:title="pageTitle" />
<!-- 是以下的简写: -->
<ChildComp :title="pageTitle" @update:title="pageTitle = $event" />
修改下上面代码,子组件 ChildComp:
<template>
<div
v-for="item in siteList"
class="site"
:class="{ 'checked': item.checked }"
:key="item.id"
@click="toggleSiteItem(item.id)"
>
{{item.id}}
</div>
</template>
<script setup name="ChildComp">
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
siteList: {
type: Array,
default: () => ([])
}
})
const emit = defineEmits(['update:siteList'])
const toggleSiteItem = (id) => {
if (!props.siteList.length) return
const _siteList = props.siteList.map((item) => {
if (item.id === id) {
return { ...item, checked: !item.checked }
}
return item
})
emit('update:siteList', _siteList)
}
</script>
ChildComp 定义了属性 siteList,然后 emit 自定义事件 update:siteList 即可
父组件 中使用:
<template>
<div>
<button @click="checkAllSiteList">全选</button>
<ChildComp v-model:siteList="siteList" />
</div>
</template>
<script setup name="ParentComp">
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
let siteList = ref([
{ id: '01', checked: false },
{ id: '02', checked: false },
])
// 全选
const checkAllSiteList = () => {
siteList.value = siteList.value.map((item) => {
if (!item.checked) return { ...item, checked: true }
return item
})
}
</script>
3. Vue3 可以在同一个组件上使用多个 v-model 绑定,像这样:
<ChildComp
v-model:title="pageInfo.title"
v-model:content="pageInfo.content"
/>
<!-- 是以下的简写: -->
<ChildComp
:title="pageInfo.title"
@update:title="pageInfo.title = $event"
:content="pageInfo.content"
@update:content="pageInfo.content = $event"
/>
const pageInfo = reactive({
title: '标题',
content: '主体内容'
})
4. Vue3.3+ 版本多了个新编译器宏 defineModel ,提供了另一种使得 v-model 更简洁的写法
子组件 ChildComp:
<template>
<button @click="handleClick">按钮</button>
</template>
<script setup>
const modelValue = defineModel()
const handleClick = () => {
modelValue.value++
}
</script>
父组件:
<template>
<div @click="changeCount">{{count}}</div>
<ChildComp v-model="count" />
</template>
<script setup>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
const count = ref(1)
const changeCount = () => {
count.value = '9999'
}
</script>
provide / inject
简单的父子组件之间通信使用 props 和 emit 就可以了,但对于多层嵌套组件的通信用这种方法会很麻烦,provide / inject 依赖注入 就是用来解决这个问题的
上层组件用 provide 提供数据:
<script setup>
import { reactive, provide } from 'vue'
const userInfo = reactive({
name: 'Stephen',
birthday: '1988-3-14'
})
provide('userInfo', userInfo)
provide('message', 'hello')
</script>
下层组件 inject 注入数据来使用:
<template>
<div>{{message}}, {{userInfo.name}}</div>
</template>
<script setup>
import { inject } from 'vue'
const userInfo = inject('userInfo')
const message = inject('message')
</script>
还可以为整个应用层 prodive 数据
// main.js
const app = createApp(App)
app.provide('userInfo', { name: 'Jack', age: 3 })
// 在组件中使用
const userInfo = inject('userInfo')
refs
如果想访问组件的某些数据和内置方法,可以通过在子组件使用 defineExpose 向外暴露数据,在父组件访问子组件的 ref 获取数据,比如 Vue2 中常用的 this.$refs.dialog.open()
子组件:
import { ref, defineExpose } from 'vue'
const dialogVisible = ref(false)
const openDialog = () => dialogVisible.value = true
// 向外暴露
defineExpose({
msg: '123',
open: openDialog
})
父组件:
<template>
<button @click="handleClick">弹出对话框</button>
<Dialog ref="dialogRef" />
</template>
<script setup>
import { ref } from 'vue'
import Dialog from '@/components/Dialog.vue'
const dialogRef = ref(null)
const handleClick = () => {
dialogRef.value.open()
}
</script>
eventBus(事件总线通信)
用于跨组件通信
bus.js:
import mitt from 'mitt'
const bus = mitt()
export default bus
组件派发信息:
<template>
<button @click="handleClick">按钮</button>
</template>
<script setup>
import bus from '@/utils/bus'
const handleClick = () => {
bus.emit('sendMsg', 'Hello!');
}
</script>
另一个组件接收信息:
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
import bus from '@/utils/bus'
const onSendMeg = () => { console.log(msg) }
onMounted(() => {
bus.on('sendMsg', onSendMeg)
})
onBeforeUnmount(() => {
bus.off('sendMsg', onSendMeg) // 组件销毁前移除监听
})
</script>
pinia
官方的 Vue 状态管理工具,类似 Vuex,但用起来更方便,摒弃了 mutations 的概念 比较像 React 的 Mobx
上个简单 Demo,用了插件 pinia-plugin-persist:
store/user.js
// 封装的用户相关的ajax请求
import { accountLogin, fetchUserInfo } from '@/services/user'
export const useUserStore = defineStore({
id: 'global',
state: () => ({
userInfo: null, // 用户信息
loginState: false, // 登录状态
}),
actions: {
// 更新用户信息
updateUserInfo(data) {
this.userInfo = _.assign({}, this.userInfo, data)
},
// 清除用户信息
clearUserInfo() {
this.userInfo = null
},
// 设置登陆状态
setLoginState(bool) {
this.loginState = bool
},
// 清除登录信息
clearLoginInfo(showMsg = false) {
this.clearUserInfo()
this.setBreadcrumbsList([])
this.setLoginState(false)
sessionStorage.removeItem('token') // 清除token
},
// ======================== effects ========================
login(username, password) {
return new Promise(async (resolve) => {
const data = await accountLogin(username, password)
if (data && data.token && data.userId) {
sessionStorage.setItem('token', data.token)
const userInfo = await fetchUserInfo(data.userId)
if (userInfo && Object.keys(userInfo).length > 0) {
this.updateUserInfo(userInfo)
this.setLoginState(true)
resolve(true)
} else {
resolve(false)
}
} else {
resolve(false)
}
})
}
},
// 插件
persist: {
enabled: true,
strategies: [
{
key: 'user_info',
storage: sessionStorage,
paths: ['userInfo']
},
{
key: 'state',
storage: sessionStorage,
paths: ['loginState']
},
]
},
})
store/index.js
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
const store = createPinia()
store.use(piniaPersist)
export default store
在组件中使用:
<template>
<div>{{userStore.loginState}}</div>
</template>
<script setup>
import { useUserStore } from '@/store/global'
const userStore = useUserStore()
</script>
pinia 更多的用法在官方上有详细说明 pinia.vuejs.org
2022.04.15 更新~~~~
slot(插槽)
父组件还可以向子组件传递模板内容,插槽就是干这个的
子组件:
<template>
<div class="child-comp">
<slot />
</div>
</template>
父组件:
<template>
<div class="parent-comp">
<ChildComp>
<p>信息:{{message}}</p>
</ChildComp>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
const message = ref('今天还要做核酸吗')
</script>
渲染出来的HTML:
<div class="parent-comp">
<div class="child-comp"><p>信息:今天还要做核酸吗</p></div>
</div>
1. 设置默认内容,如果不传模板内容 就显示设置的默认内容
<div class="child-comp">
<slot>默认信息</slot>
</div>
2. 具名插槽
当一个组件需要多个插槽口,可以在 <slot> 上增加 name 属性来标识,如果不写 name 默认为 name="default",比如一个简化的弹窗组件:
<template>
<div class="dialog">
<div class="dialog-hd">
<slot name="header">提示</slot>
</div>
<div class="dialog-bd">
<slot name="default" />
</div>
<div class="dialog-ft">
<slot name="footer">
<button>取消</button>
<button>确定</button>
</slot>
</div>
</div>
</template>
<script setup name="Dialog">
// ...
</script>
父组件中使用:
<Dialog>
<template #header>
删除操作
</template>
<template #default>
是否确认删除改条数据?
</template>
</Dialog>
用 <template> 来包裹要插入的模板内容,#header 是 v-slot:header 的简写,参数为子组件具名插槽相应的 name 值
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非
<template>节点都被隐式地视为默认插槽的内容
比如这里的 <p>描述一</p><p>最好别删哦</p> 会被插入 <slot name="default"></slot> 中,但从可维护性角度来看 还是建议用 <template #default></template> 包裹
<Dialog>
<template #header>
删除操作
</template>
<p>描述一</p>
<p>最好别删哦</p>
</Dialog>
插槽名还可以是动态的,修改下上面的例子:
<template>
<Dialog>
<template #[slotName]>
删除操作
</template>
<template #default>
是否确认删除改条数据?
</template>
</Dialog>
</template>
<script setup>
import { ref } from 'vue'
const slotName = ref('header')
</script>
3. 插槽使用子组件的数据
默认情况下插槽是无法访问子组件中数据,但可以在子组件的 <slot> 中传递 props 让父组件中插槽拿到数据
子组件:
<template>
<div class="child-comp">
<slot nickName="Curry" :age="age" />
</div>
</template>
<script setup name="ChildComp">
import { ref } from 'vue'
const age = ref(18)
</script>
父组件通过 v-slot 获取属性:
<div class="parent-comp">
<ChildComp v-slot="slotProps">
<div>{{slotProps.nickName}}</div>
<div>{{slotProps.age}}</div>
</ChildComp>
</div>
也可以直接在 v-slot 中解构
<div class="parent-comp">
<ChildComp v-slot="{ nickName, age }">{{nickName}}</ChildComp>
</div>
具名插槽使用子组件数据时,和默认插槽有些区别,直接看下面 demo
子组件:
<template>
<div class="child-comp">
<header>
<slot name="header" title="提示" :content="content" />
</header>
<div>
<slot name="default" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const content = ref('朋友~ 别再内卷了')
</script>
父组件模板中使用:
<div class="parent-comp">
<ChildComp>
<template #header="headerProps">
<p>一条消息:</p>
<p>{{headerProps.title}},{{headerProps.content}}</p>
</template>
</ChildComp>
</div>
渲染出来的结果:
先这样了~ 以上七种组件之间通信的方式 可以应对大多数情况