目录
前言
常见的组件关系有:父子组件、祖孙组件、兄弟组件以及任意组件,不同组件之间数据传递有不同的方法。
父传子:
props、v-model、$refs、默认插槽、具名插槽
子传父:
props、自定义事件、v-model、$parent、作用域插槽
祖传孙、孙传祖:
$arrts、provide、inject
兄弟组件、任意组件之间传值:
mitt、pinia
一、props
props通常用于父子组件之间传值,父传子属性是非函数,子传父属性是函数。
父组件:
<template>
<div class="app">
<h1>父组件</h1>
<div>接收到的子组件数据:{{ getChildData }}</div>
<Child :data="parentMsg" :updataChildData="toUpdataChildData"></Child>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './components/child.vue'
const parentMsg = ref('你好')
const getChildData = ref(null)
const toUpdataChildData = (childData) => {
getChildData.value = childData
}
</script>
<style lang="scss" scoped>
.app{
padding: 20px;
background-color: skyblue;
border-radius: 10px;
}
</style>
子组件child.vue
<template>
<div class="container">
<h1>子组件</h1>
<div>接收到的父组件数据:{{ data }}</div>
<button @click="updataChildData(childData)">传值给父组件</button>
</div>
</template>
<script setup>
import { ref } from "vue"
const props = defineProps(['data', 'updataChildData'])
const childData = ref(1000)
</script>
<style lang="scss" scoped>
.container{
background-color: pink;
padding: 20px;
border-radius: 10px;
}
</style>
二、自定义事件
自定义事件通常用于子组件给父组件传值,在子组件标签中绑定自定义事件,然后在子组件内通过emit触发自定义事件并传递数据。
父组件:
<template>
<div class="app">
<h1>父组件</h1>
<div>子组件传递过来的数据:{{ data }}</div>
<Child @send-data="giveChildData"></Child>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './components/child.vue'
const data = ref(null)
const giveChildData = (childData) => {
data.value = childData
}
</script>
子组件:
<template>
<div class="container">
<h1>子组件</h1>
<div>子组件的数据:{{ msg }}</div>
<button @click="emit('send-data', msg)">传递数据</button>
</div>
</template>
<script setup>
import { ref } from "vue"
const msg = ref('你好')
const emit = defineEmits(['send-data'])
</script>
三、mitt
mitt相当于vue2中的事件总线,利用它可以实现任意组件之间的数据传递。
1、安装
npm i mitt
2、创建
src/utils/emitter.js
// 引入mitt
import mitt from "mitt"
// 创建emitter
const emitter = mitt()
export default emitter
3、使用
接收数据的组件:
import emitter from "@/utils/emitter"
import { onUnmounted } from "vue"
// 绑定事件
emitter.on('send-data',(value)=>{
console.log('获取到的数据',value)
})
onUnmounted(()=>{
// 解绑事件
emitter.off('send-data')
})
提供数据的组件:
import emitter from "@/utils/emitter"
const sendData = () => {
// 触发事件
emitter.emit('send-data',data)
}
四、v-model
v-model可以实现父子组件之间的相互通信。
🌱 input表单元素上v-model的本质:
<!-- 使用v-model指令 -->
<input type="text" v-model="userName">
<!-- v-model本质 -->
<input
type="text"
:value="userName"
@input="userName = "$event.target.value"
>
🌱 组件标签上使用v-model指令,它的本质是相当于给子组件传递一个props(modelValue)并且绑定一个自定义事件update:modelValue,从而实现父子组件之间的数据同步。只使用一个v-model时,不需要指定变量名,默认是modelValue。
<!-- 组件标签上使用v-model指令 -->
<CustomInput v-model="userName"/>
<!-- 组件标签上v-model的本质 -->
<CustomInput :modelValue="userName" @update:model-value="userName = $event"/>
CustomInput.vue
<template>
<div class="box">
<input
type="text"
:value="modelValue"
@input="emit('update:model-value',$event.target.value)"
>
</div>
</template>
<script setup name="AtguiguInput">
// 接收props
defineProps(['modelValue'])
// 声明事件
const emit = defineEmits(['update:model-value'])
</script>
🌱 v-model还可以同时绑定多个值:
父组件:
<template>
<div class="app">
<h1>父组件</h1>
<div>a:{{ a1 }},b:{{ b1 }},c:{{ c1 }}</div>
<button @click="updateData">更新数据</button>
<Child
v-model:a="a1"
v-model:b="b1"
v-model:c="c1"
></Child>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './components/child.vue'
const a1 = ref(111)
const b1 = ref(222)
const c1 = ref(333)
const updateData = () => {
a1.value += 10
b1.value += 10
c1.value += 10
}
</script>
子组件child:
<template>
<div class="container">
<h1>子组件</h1>
<div>a:{{ a }},b:{{ b }},c:{{ c }}</div>
<button @click="updateData">更新数据</button>
</div>
</template>
<script setup>
const props = defineProps(['a', 'b', 'c'])
const emit = defineEmits(['update:a', 'update:b', 'update:c'])
const updateData = () => {
emit('update:a', 111000)
emit('update:b', 222000)
emit('update:c', 333000)
}
</script>
五、$attrs
$attrs可以实现祖组件向孙组件传值,每个子组件都可以通过$attrs将没有被props接收的数据传递给它的子组件。
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<Child :a="a" :b="b" :c="c" />
</div>
</template>
<script setup name="Father">
import Child from './Child.vue'
import { ref } from "vue"
let a = ref(1)
let b = ref(2)
let c = ref(3)
</script>
子组件:
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup name="Child">
import GrandChild from './GrandChild.vue'
</script>
孙组件:
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
</div>
</template>
<script setup name="GrandChild">
defineProps(['a','b','c'])
</script>
👉ps:$attrs可以多层传递。
六、$refs、$parent
$refs用于父组件给子组件传值,通过$refs可以获取到子组件中向外暴露的所有方法和数据,$parent用于子组件给父组件传值,通过$parent可以获取到父组件中向外暴露的所有方法和数据。
使用ref需要在子组件标签上通过ref属性给它定义一个唯一标识,然后通过ref函数创建对子组件的引用,通过这个引用可以获取到子组件中向外暴露的所有方法和数据。
$refs:包含所有被ref
属性标识的DOM
元素或组件实例。
$parent:当前组件的父组件实例对象。
父组件:
<template>
<div class="app">
<h1>父组件</h1>
<div>父组件number值:{{ number }}</div>
<div>子组件1的数据:{{ msg1 }}</div>
<div>子组件2的数据:{{ msg2 }}</div>
<button @click="updateData">给子组件传递数据</button>
<button @click="getAllChild($refs)">获取所有被ref属性标识的组件</button>
<!-- 通过ref标识子组件 -->
<Child1 ref="child1"></Child1>
<Child2 ref="child2"></Child2>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import Child1 from './components/child1.vue'
import Child2 from './components/child2.vue'
const msg1 = ref('')
const msg2 = ref('')
const number = ref(10000)
// 创建对子组件的引用
const child1 = ref()
const child2 = ref()
// 给子组件传递数据
const updateData = () => {
child1.value.childData += '6666'
child2.value.childData += '7777'
}
// 获取子组件数据
onMounted(() => {
msg1.value = child1.value.childData
msg2.value = child2.value.childData
})
const getAllChild = (refs) => {
console.log(refs);
for (let key in refs){
refs[key].childData += '~~~~'
}
}
// 需要通过defineExpose暴露出去,子组件才能获取到
defineExpose({
number
})
</script>
子组件child1:
<template>
<div class="container">
<h1>子组件1</h1>
<div>子组件的数据:{{ childData }}</div>
<button @click="subNum($parent)">父组件的number值减100</button>
</div>
</template>
<script setup>
import { ref } from "vue"
const childData = ref('你好')
const subNum = (parent) => {
parent.number -= 100
}
// 需要通过defineExpose暴露出去,父组件才能获取到
defineExpose({
childData
})
</script>
子组件child2:
<template>
<div class="container">
<h1>子组件2</h1>
<div>子组件的数据:{{ childData }}</div>
</div>
</template>
<script setup>
import { ref } from "vue"
const childData = ref('hello')
// 需要通过defineExpose暴露出去,父组件才能获取到
defineExpose({
childData
})
</script>
七、provide、inject
provide和inject可以实现祖孙组件之间的通信。祖先组件通过provide向其后代组件传递数据,后代组件使用inject获取数据。
父传子,传递变量;子传父,传递函数。
父组件:
<template>
<div class="app">
<h1>父组件</h1>
<div>父组件数据:姓名{{ data.name }},年龄:{{ data.age }}</div>
<button @click="updateData">更新数据</button>
<Child></Child>
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
import Child from './components/child.vue'
const data = ref({
name: 'zhangsan',
age: 19
})
const updateAge = (sendAge) => {
data.value.age = sendAge
}
provide('parentData', data)
// 传递函数给后代组件,可以让后代组件向父组件传值
provide('updateAge', updateAge)
const updateData = () => {
data.value.age += 1
data.value.name += '~'
}
</script>
子组件child:
<template>
<div class="container">
<h1>子组件1</h1>
<div>获取到的父组件数据:姓名{{ info.name }},年龄:{{ info.age }}</div>
<GrandChild />
</div>
</template>
<script setup>
import GrandChild from './grandChild.vue'
import { inject, ref } from "vue"
// 第一个参数是key值,第二个参数是默认值
const info = inject('parentData', {
name: '未知',
age: '未知'
})
</script>
孙组件grandChild:
<template>
<div class="container1">
<h1>孙组件</h1>
<div>获取到的父组件数据:姓名{{ info.name }},年龄:{{ info.age }}</div>
<button @click="updateAge(info.age+10)">年龄+10</button>
</div>
</template>
<script setup>
import { inject, ref } from "vue"
const info = inject('parentData', {
name: '未知',
age: '未知'
})
const updateAge = inject('updateAge')
</script>
八、pinia
相当于vue2中的vuex,pinia也是vue.js状态管理库。
1、搭建pinia环境
下载
npm install pinia
创建
src/main.js
import { createApp } from 'vue'
import App from './App.vue'
/* 引入createPinia,用于创建pinia */
import { createPinia } from 'pinia'
/* 创建pinia */
const pinia = createPinia()
const app = createApp(App)
/* 使用插件 */
app.use(pinia)
app.mount('#app')
2、存储数据
它有三个概念:store、getters、actions,相当于组件中的data、computed和methods。
src/store/count.js
// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useCountStore = defineStore('count',{
// 动作
actions:{
increment(value) {
this.sum += value
}
},
// 状态
state(){
return {
sum:6,
a:1,
b:2,
c:3
}
},
// 计算
getters:{
bigSum:(state) => state.sum *10,
}
})
actions中可以通过this来获取到state中的数据,getters用于处理state中的数据。
3、组件中使用数据
<template>
<h2>state中的数据:{{ countStore.sum }}</h2>
<h2>getters处理的数据:{{ countStore.bigSum }}</h2>
<button @click="add">点击加三</button>
</template>
<script setup name="Count">
// 引入对应的useXxxxxStore
import {useCountStore} from '@/store/count'
// 调用useXxxxxStore得到对应的store
const countStore = useCountStore()
const add = () => {
//调用actions中的方法
countStore.increment(3)
}
</script>
4、修改数据
(1)直接修改
countStore.sum = 666
(2)批量修改
countStore.$patch({
sum:999,
a:11,
b:22,
c:33
})
(3)通过actions修改,然后在组件中调用actions中的方法
5、storeToRefs
使用storeToRefs可以将store中的数据转换成ref对象。
注意:pinia中的storeToRefs只会对数据本身转换,而vue中的toRef会转换store中的数据。
我们通过解构拿到的数据不是响应式的,所以需要使用storeToRefs将它们变成响应式的。
<template>
<div class="count">
<h2>当前求和为:{{sum}}</h2>
</div>
</template>
<script setup lang="ts" name="Count">
import { useCountStore } from '@/store/count'
/* 引入storeToRefs */
import { storeToRefs } from 'pinia'
/* 得到countStore */
const countStore = useCountStore()
/* 使用storeToRefs转换countStore,随后解构 */
const {sum} = storeToRefs(countStore)
</script>
6、$subscribe
使用$subscribe方法可以侦听store中的数据变化
countStore.$subscribe((mutate,state)=>{
console.log(mutate, state);
})
7、组合式写法
import {defineStore} from 'pinia'
import {reactive} from 'vue'
export const useTalkStore = defineStore('talk',()=>{
// talkList就是state
const talkList = reactive({})
// getATalk函数相当于action
function getATalk(){}
return {
talkList,
getATalk
}
})
九、slot
1、默认插槽
组件标签之间的内容会被放到默认插槽<slot></slot>当中,如果没有默认插槽,则这些内容不会显示出来。
父组件中:
<Child :title="title">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Child>
Child子组件中:
<template>
<div class="item">
<h3>{{ title }}</h3>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>
2、具名插槽
父组件中:
<Child :title="title">
<template v-slot:s1>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
<template #s2>
<div>你好</div>
</template>
</Child>
Child子组件中:
<template>
<div class="item">
<h3>{{ title }}</h3>
<slot name="s1"></slot>
<slot name="s2"></slot>
</div>
</template>
使用时,有两种写法:
(1)v-slot:name
(2)#name
内容会被放到对应名字的插槽中
3、作用域插槽
数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定,相当于子组件给父组件传值。
父组件中:
<Game v-slot="params">
<ul>
<li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</Game>
子组件中:
<template>
<div class="category">
<h2>今日游戏榜单</h2>
<slot :games="games" a="哈哈"></slot>
</div>
</template>
<script setup lang="ts" name="Category">
import {reactive} from 'vue'
let games = reactive([
{id:'asgdytsa01',name:'英雄联盟'},
{id:'asgdytsa02',name:'王者荣耀'},
{id:'asgdytsa03',name:'红色警戒'},
{id:'asgdytsa04',name:'斗罗大陆'}
])
</script>