1.props传递
父传子
数据是单项绑定的,属于只读。
<!-- 父组件传递apple -->
<Son :apple="apple"/>
// 子组件接收apple
const props = defineProps(['apple'])
子传父
<!-- 父组件传递sendOrange方法到子组件 -->
<Son :sendOrange="getOrange"/>
// 子组件接收方法,调用时传递参数
const props = defineProps(['sendOrange'])
props.sendOrange('橘子')
// 父组件的getOrange方法被调用,参数已经拿到了
function getOrange(val: string){
console.log(`我吃了儿子的一口${val}`)
}
2.自定义事件
子传父
<!-- 父组件绑定自定义事件sendOrange到子组件 -->
<Son @sendOrange="getOrange"/>
// 子组件使用defineEmits定义sendOrange方法
const emit = defineEmits(['sendOrange'])
onMounted(() => {
setTimeout(() => {
emit('sendOrange', '橘子') // 携带参数触发sendOrange的回调
}, 2000)
})
// 父组件中自定义事件sendOrange的回调getOrange方法被触发
function getOrange(val){
console.log(`我吃了儿子的一口${val}`)
}
3.mitt传递
全局通信
// 引入mitt
npm i mitt
// src/utils/emmiter.ts
// 创建并暴露emmiter.ts
import mitt from "mitt"
const emitter = mitt() // 这个相当于工具,身上带有各种方法比如 on emit off all
export default emitter
// src/components/Son1.vue
import emitter from "@/utils/emitter"; // 引入emitter
onMounted(() => {
// 在哥哥组件里订阅消息,准备获取弟弟组件的数据
emitter.on('getSmallBroData',(val: any) => {
youngerBroData.value = val
})
})
// 组件卸载时记得取消订阅消息
onBeforeMount(() => {
emitter.off('getSmallBroData')
})
// src/components/Son2.vue
import emitter from "@/utils/emitter";
onMounted(() => {
setTimeout(() => {
// 三秒后发布消息,发送数据到哥哥那边
emitter.emit('getSmallBroData', '弟弟的数据')
}, 3000);
})
4.v-model传递(*重点*)
理解
自定义封装Input组件时,使用v-model就属于父子组件互相传递参数,拆解后分为两点:普通props传递与自定义事件传递,将这两点组合起来,再进行简写,就是v-model传递了,常用在输入类组件的自定义封装。
使用方法
假如我现在要自定义封装一个input框,我应该怎么做呢?
- 创建一个普通子组件:TestInput.vue。
- 在父组件中引入子组件<test-input/>。
- 注意,要封装的是input组件,就像Element-UI中的<el-input v-model="xxx">一样,我们也应该写成<test-input v-model="xxx"/>。
// src/components/Father.vue <template> <test-input v-model="data"/> </template> <script setup lang="ts"> import { ref } from "vue"; import TestInput from "@/components/TestInput.vue"; const data = ref('默认值') </script> <style scoped> </style>
- 子组件应该如何接收这个v-model呢?这里直接提供答案,在后面的底层逻辑会详细讲解。(这里都是固定写法哦,命名是不能改变的,这是由Vue3开发者规定的)
// src/components/TestInput.vue <template> <input type="text" :value="modelValue" @input="emit('update:modelValue', $event.target.value)" > </template> <script setup lang="ts"> defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script>
- 创建多个具有多个v-model的组件TestForm.vue
v-model命名:接收v-model:// src/components/Father.vue <test-form v-model:name="nameVal" v-model:age="ageVal"/>
// src/components/TestForm.vue <input type="text" :value="name" @input="emit('update:name', $event.target.value)" /> <input type="text" :value="age" @input="emit('update:age',$event.target.value)" />
// src/components/TestForm.vue defineProps(['name','age']) const emit = defineEmits(['update:name', 'update:age']
底层原理
没研究过底层逻辑的朋友看到上面的使用方法会一脸懵B,
有疑问的朋友别急,先看这个例子:<input type="text" v-model="data"/>
把v-model换成它最基本的样子:
<input type="text" :value="data" @input="data=$event.target.value"/>
- :value="data" 实现了单向绑定,将数据呈现到了页面上;
- @input="data=$event.target.value" 随着input事件被触发,将最新的值赋值给data。
这两种相扶而成,就成为了数据的双向绑定,而v-model也成为了这种写法的简写形式。
那么如果是自定义的input组件该怎么写?跟原生input很相似,有些许的不同:
<test-input :modelValue="data" @update:modelValue="data = $event"/>
- :modelValue="data" 向子组件传递props参数。(命名由Vue3开发者决定,写死了。)
- @update:modelValue="data = $event" 给子组件绑定自定义事件,当这个自定义方法被调用时,就将$event赋值给data。(注意!它的事件名是带有分号的,这不是你没有学过的新语法,命名由Vue3开发者决定,写死了。)
所以朋友们,我们再结合这段代码来看看:
// src/components/TestInput.vue <template> <input type="text" :value="modelValue" @input="emit('update:modelValue', $event.target.value)" > </template> <script setup lang="ts"> defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script>
- modelValue是什么?
解答:就只是个props参数而已,用之前记得defineProps声明一下。- update:modelValue又是什么?
解答:一个带分号的自定义事件名。- input的事件回调为什么要触发update:modelValue这个方法?
解答:简单来说,父向子传递两个参数(简写形式为v-model),一个是数据(modelValue),一个是自定义方法(update:modelValue),子拿着数据来展示,拿着自定义方法等待着@input事件触发,一旦触发,子会将最新数据通过这个自定义方法传递给父,实现了父子组件之间的数据通信。- 执行顺序为?
解答:
①父向子传入props参数用于初始化子组件的:value;父中给子绑定自定义事件,在其回调中写入驱动data实时改变的代码并等待执行;②用户敲下键盘,子组件中原生input事件被触发,在原生事件@input的回调中使用emit手动触发自定义方法,并向父传递最新数据($event.target.value)。<!-- 这里的回调有几种写法,官方推荐第一种 --> <!-- 为什么这里不能$event.target.value? --> <!-- 1.因为这个是自定义组件而不是input元素--> <!-- 2.$event的值就是自定义方法使用emit调用时传递的参数的值 --> <test-input :modelValue="data" @update:modelValue="data = $event;"/> <!-- 其他写法 --> <test-input :modelValue="data" @update:modelValue="($event) => {data = $event}"/> <test-input :modelValue="data" @update:modelValue="(val) => {data = val}"/>
③父组件在自定义方法被触发后的回调中使用$event接收参数,对data进行赋值,结束该事件流,完成了利用v-model的数据传递。
5.$attrs传参
理解
浅层级组件以props方式向更深层级组件传参,在此过程中,被defineProps定义的参数将会从$attrs中取出并放在对应组件的props中,而未被defineProps定义的参数将会在$attrs中继续往更深层级传递。
使用场景
跨级传参:父传孙或者孙传父
使用方法
// 父组件:向子组件传递3个参数
<Child :fatherData=fatherData :getGrandChildData="getGrandChildData" :x="123"/>
// 子组件:定义x参数并使用v-bind加$attrs的方式继续向下传递剩余参数
<grandChild v-bind="$attrs"/>
const props = defineProps(['x'])
// 孙组件:能拿到剩余2个参数并使用,拿不到x参数了。
const props = defineProps(['fatherData', 'getGrandChildData'])
6.$refs与$parents传参
理解
非常简单,事件传参时使用$refs占位符能获取所有被ref属性标记的子组件实例,使用$parents能获取父组件实例,需要特别注意的是:不管是$refs还是$parents,拿到对应组件实例后不等同拿到了它的数据,要记得在组件实例中使用defineExpose({xxx})将数据暴露出去。
使用场景
$refs:父组件中批量修改子组件的状态
$parents:子组件中修改父组件的状态
使用方法
$refs:
<!-- 父组件:$refs包含父组件中所有被ref标记的组件实例 --> <Child1 ref="child1Ref"/> <Child2 ref="child2Ref"/> <button @click="changeChildData($refs)">点我批量更改子组件的数据</button>
// 父组件:批量修改子组件状态 function changeChildData($refs:any){ Object.keys($refs).forEach(keyName => { $refs[keyName].childData = '我们虽然不是同一个子组件,但我们有同一个爸爸!' }) }
// 子组件: const childData = ref('我是Child可以暴露的数据') defineExpose({childData})
$parents:
// 父组件 const fatherData = ref(0) defineExpose({fatherData})
<!-- 子组件:$parent就是父组件实例 --> <button @click="changeFatherData($parent)">点我更新父亲的数据</button>
// 子组件:修改父组件的状态 function changeFatherData($parent:any){ $parent.fatherData+=1 }
7.provide与inject传参
理解
provide
和 inject
是用于实现依赖注入的 API。它们的主要作用是在组件树中跨层级传递数据或方法,而不需要通过 props
逐层传递。这种方式特别适合在深层嵌套的组件之间共享数据或逻辑。
使用场景
-
跨层级组件通信:
父组件需要向深层嵌套的子组件(如孙组件、曾孙组件等)传递数据或方法,避免通过
props
逐层传递,简化代码结构。 -
共享全局状态:
在组件树的顶层提供全局状态(如用户信息、主题配置等),供所有子组件使用。
-
插件或工具方法注入:
在根组件中提供一些工具方法或插件,供所有子组件使用。
使用方法
-
provide:
用于在祖辈组件中提供数据或方法,供后代组件注入使用。// 祖辈组件 import { provide, ref } from 'vue'; const message = ref('Hello from parent'); provide('message', message);
-
inject:用于在后代组件中注入祖辈组件提供的数据或方法。
// 子组件 import { inject } from 'vue'; // 注入数据 const message = inject('message', 'Default Message');
注意:如果遇到重名的情况,Vue 会以就近原则为准,即会使用最近的父级组件提供的同名属性进行注入。这意味着如果有多个父级组件提供了同名属性,那么被注入的属性将取决于组件树中的层次关系。Vue 不会报错,但是可能会导致意外的结果,因此在设计组件时应尽量避免出现重名的情况,或者使用更具有唯一性的命名方式。
8.slot插槽
理解
1. 什么是插槽?
插槽是 Vue 组件中的一个占位符,允许父组件向子组件传递内容(HTML 结构、文本、其他组件等)。子组件通过插槽定义内容的位置,父组件通过插槽填充内容。
2. 插槽的作用
①内容分发:将父组件的内容插入到子组件的指定位置。
①组件复用:通过插槽,组件可以更灵活地适应不同的使用场景。
①解耦 UI 和逻辑*:子组件负责获取或管理数据,但不同父组件可能需要用不同方式展示这些数据。
使用场景
封装灵活度的组件会用到
使用方法
这里以封装一个简易的模态框为例子,父组件负责控制HTML解构和样式,子组件只处理逻辑和获取数据。
父组件:
- #header其实就是v-slot:header的缩写,其余插槽也一样。
- 这里没写默认插槽,如果是默认插槽想要接收子组件的参数,就用#defualt="xxxx"
- 参数取出来后是一个包含子组件所有通过v-bind传来的参数的对象,记得解构再使用。
<template>
<div>
<button @click="modalRef.openModal()">打开模态框</button>
<Modal ref="modalRef">
<template #header">
<h1>{{ headerName }}</h1>
</template>
<template #content="{list}">
<ul>
<li v-for="item in list" :key="item.id">
{{ item.data }}
</li>
</ul>
</template>
<template #footer>
<div>
<button @click="modalRef.closeModal()">确定</button>
<button @click="modalRef.closeModal()">取消</button>
</div>
</template>
</Modal>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Modal from "@/components/Modal.vue";
const modalRef = ref()
const headerName = ref('我是标题xxx')
</script>
<style scoped>
</style>
子组件:
- slot标签name="header"跟父组件中的#header(v-slot:header)完成对应。
- slot标签可以通过v-bind(这里是简写形式)给父组件传参。
- slot标签之间可以写默认值,没值的时候就会展示默认值。
<template>
<div v-if="isShow">
<button @click="closeModal">关闭模态框</button>
<slot name="header">没传值就显示这段默认内容</slot>
<slot name="content" :list="list"></slot>
<slot name="footer">没传值就显示这段默认内容</slot>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue"
const isShow = ref(false)
const list = reactive<any>([])
onMounted(() => {
// 模拟获取数据
setTimeout(() => {
Object.assign(list, [
{ id: 1, data: '1'},
{ id: 2, data: '2'},
{ id: 3, data: '3'}
])
// 模拟处理数据
list.forEach(item => item.data = Number(item.data))
}, 1000)
})
function openModal(){
isShow.value = true
}
function closeModal(){
isShow.value = false
}
defineExpose({openModal, closeModal})
</script>
<style scoped>
</style>
拓展
想法: 如果另一个父组件需要使用这个模态框,但获取的数据的方式完全不同,原组件就无法复用,因为数据被写死了。
解决思路:父来获取数据,用props(v-bind)传递给子,子拿到数据只进行逻辑处理,再以参数的形式传回给父,父拿到处理过的数据来控制具体的UI展现形式,这样搞灵活性可以大大增加。