Vue3 --- 组件通信

1. 父向子

1. props

0. 定义数据需要实现的接口

export interface Person {
    id: string
    name: string;
    age: number;
}

// export type PersonList = Array<Person>;
export type PersonList = Person[];

1. 父组件中, 在子组件实例标签中以参数的形式传递数据

<template>
  <!-- 1. 使用 :变量名 给子组件传递非字符串类型的数据 -->
  <Person :personList="personList"/>
</template>

<script setup lang="ts" name="Father">

import Person from "@/components/Person.vue";
import {reactive} from "vue";
import {type PersonList} from '@/types'

let personList = reactive<PersonList>([
  {id: "asdfasdgasd01", name: "小明", age: 20},
  {id: "asdfasdgasd02", name: "小红", age: 15},
  {id: "asdfasdgasd03", name: "小白", age: 10}
])

</script>

<style scoped>
</style>

2. 子组件中声明接收父组件传递过来的数据

0. 只接受数据

<template>
  <!-- 2. 页面中使用父组件传递过来的数据 -->
  <ul>
    <li v-for="p in personList" :key="p.id">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
  </ul>
</template>

<script setup lang="ts" name="Person">

// 1. 接收父组件传来的参数
defineProps(['personList'])

</script>

<style scoped>
.app {
  background-color: aqua;
}
</style>

1. 读取数据

<template>
  <!-- 3. 页面中使用父组件传递过来的数据 -->
  <ul>
    <li v-for="p in personList" :key="p.id">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
  </ul>
</template>

<script setup lang="ts" name="Person">
import {defineProps} from 'vue';  // defineProps 可以不引入, vue 内部已经缓存了这个函数
    
// 1. 接收父组件传来的参数, 并赋值
const props = defineProps(['personList'])
console.log(props.personList[0].id)   // 2. 取值

</script>

<style scoped>
.app {
  background-color: aqua;
}
</style>

2. 接收数据,并限制数据符合指定接口的类型

<template>
  <!-- 3. 页面中使用父组件传递过来的数据 -->
  <ul>
    <li v-for="p in personList" :key="p.id">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
  </ul>
</template>

<script setup lang="ts" name="Person">
import {defineProps} from 'vue';  // defineProps 可以不引入, vue 内部已经缓存了这个函数
import {type PersonList} from '@/types'


// 1. 接收父组件传来的参数, 并赋值, 通过 PersonList 接口来限制类型和字段
const props = defineProps<{ personList: PersonList }>()
console.log(props.personList[0].id)   // 2. 取值

</script>

<style scoped>
.app {
  background-color: aqua;
}
</style>

3. 接收数据, 并限制数据符合指定接口的类型, 限制必要性, 指定默认值

<template>
  <!-- 3. 页面中使用父组件传递过来的数据 -->
  <ul>
    <li v-for="p in personList" :key="p.id">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
  </ul>
</template>

<script setup lang="ts" name="Person">
import {defineProps} from 'vue';   // defineProps 可以不引入, vue 内部已经缓存了这个函数
import {type PersonList} from '@/types'


// 1. 接收父组件传来的参数, 并赋值, 通过 PersonList 接口来限制类型和字段
// 使用 ? 来指定非必要的限制
// 使用 withDefaults 来设置默认值
withDefaults(defineProps<{ personList?: PersonList }>(), {
  personList: () => [{
    id: "asdfasdgasd01",
    name: "小明",
    age: 20
  }]
})

</script>

<style scoped>
.app {
  background-color: aqua;
}
</style>

2. 通过获取子组件实例操作子组件的数据

1. ref 标识

src/components/Father.vue

<template>
    <div class="app">
        <h2>我是父组件</h2>
        <h4>房产: {{ houseNum }}</h4>
        <h4>来自儿子组件的数据: {{ defaultData }}</h4>
        <button @click="changeComputer">修改 Son1 的电脑</button>
        <button @click="changeToy">修改 Son2 的玩具</button>
        <!-- 1. 使用 ref 来标记子组件实例 -->
        <Son1 ref="son1"/>
        <Son2 ref="son2"/>
    </div>
</template>

<script lang="ts" setup name="App">
import Son1 from '@/components/Son1.vue';
import Son2 from '@/components/Son2.vue';
import { ref } from 'vue';

let defaultData = ref('')
let houseNum = ref(4)

// 2. 取出子组件实例 
let son1 = ref()
let son2 = ref()

// 3. 想要修改儿子组件的数据, 需要儿子组件中使用 defineExpose 对外暴露数据
function changeComputer(){
    son1.value.computer = "华为笔记本"
}

function changeToy(){
    son2.value.toy = "游戏机"
}


</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/Son1.vue

<template>
  <div class="son">
    <h2>我是儿子1组件</h2>
    <h4>玩具: {{ computer }}</h4>
    <h4>书: {{ book }}</h4>
  </div>
</template>

<script lang="ts" setup name="Son">
import { ref } from 'vue';

const computer = ref("苹果笔记本")
let book = ref(6)

// 1. 向外暴露 toy
defineExpose({
  computer
})
</script>

<style scoped>
.son{
    width: 1200px;
    height: 250px;
    border-radius: 20px;
    background-color: cadetblue;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

src/components/Son2.vue

<template>
    <div class="grandson">
        <h2>我是儿子2组件</h2>
        <h4>玩具: {{ toy }}</h4>
        <h4>书: {{ book }}</h4>
    </div>
</template>

<script lang="ts" setup name="GrandSon">
import { ref } from 'vue';

const toy = ref("奥特曼")
let book = ref(3)

// 1. 向外暴露 toy
defineExpose({
    toy
})
</script>

<style scoped>
.grandson{
    width: 1200px;
    height: 250px;
    border-radius: 20px;
    background-color: brown;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

2. $refs

src/components/Father.vue

<template>
    <div class="app">
        <h2>我是父组件</h2>
        <h4>房产: {{ houseNum }}</h4>
        <h4>来自儿子组件的数据: {{ defaultData }}</h4>
        <!-- 2. 将 $refs 当做参数 传进回调函数中, $refs 就是所有的子组件实例数组 -->
        <button @click="allSonBuyBook($refs)">所有孩子的数加三本</button>
        <!-- 1. 使用 ref 来标记子组件实例 -->
        <Son1 ref="son1"/>
        <Son2 ref="son2"/>
    </div>
</template>

<script lang="ts" setup name="App">
import Son1 from '@/components/Son1.vue';
import Son2 from '@/components/Son2.vue';
import { ref } from 'vue';

let defaultData = ref('')
let houseNum = ref(4)

function allSonBuyBook(refs:any){
    // refs 就是所有的子组件实例
    for (const key in refs) {
        refs[key].book += 3
    }
}

</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/Son1.vue

<template>
  <div class="son">
    <h2>我是儿子1组件</h2>
    <h4>玩具: {{ computer }}</h4>
    <h4>书: {{ book }}</h4>
  </div>
</template>

<script lang="ts" setup name="Son">
import { ref } from 'vue';

const computer = ref("苹果笔记本")
let book = ref(6)

// 1. 向外暴露 toy
defineExpose({
  computer,
  book
})
</script>

<style scoped>
.son{
    width: 1200px;
    height: 250px;
    border-radius: 20px;
    background-color: cadetblue;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

src/components/Son2.vue

<template>
    <div class="grandson">
        <h2>我是儿子2组件</h2>
        <h4>玩具: {{ toy }}</h4>
        <h4>书: {{ book }}</h4>
    </div>
</template>

<script lang="ts" setup name="GrandSon">
import { ref } from 'vue';

const toy = ref("奥特曼")
let book = ref(3)

// 1. 向外暴露 toy
defineExpose({
    toy,
    book
})
</script>

<style scoped>
.grandson{
    width: 1200px;
    height: 250px;
    border-radius: 20px;
    background-color: brown;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

2. 子向父

1. 自定义函数

src/components/Father.vue

<template>
  <div class="app">
    <div>
        接收到来自子组件的数据: <span>{{ toy }}</span>
    </div>
    <!-- 2. 将定义好的函数传递给子组件, 名字叫 sendToy -->
    <Home :sendToy="getToy"/>
  </div>
</template>


<script lang="ts" setup name="App">
import { ref } from 'vue';
import Home from './components/Home.vue';

let toy = ref('')

// 1. 定义用来接收子组件数据的函数
function getToy(value: string){
    console.log("父组件接收到来自子组件的数据: ", value);
    toy.value = value
}

</script>

<style scoped>
</style>

src/components/Son.vue

<template>
<h2>我是子组件Home</h2>
<div>
    子组件的数据: <span>{{ toy }}</span>
</div>
<!-- 2. 直接调用父组件传递的 sendToy 函数, 并填写参数 -->
<button @click="sendToy(toy)">把玩具发送给父组件</button>
</template>

<script lang="ts" setup name="Home">
import { ref } from 'vue';

const toy = ref("奥特曼")

// 1. 接受来自父组件用来发送数据的方法 sendToy
defineProps(['sendToy'])

</script>

2. 自定义事件

src/components/Father.vue

<template>
  <div class="app">
    <div>
        接收到来自子组件的数据: <span>{{ toy }}</span>
    </div>
    <!-- 1. 给子组件绑定一个自定义事件 -->
    <Home @send-toy="saveToy"/>
  </div>
</template>


<script lang="ts" setup name="App">
import { ref } from 'vue';
import Home from './components/Home.vue';

let toy = ref('')

// 2. 定义自定义事件的回调函数, 并接收子组件传递的参数
function saveToy(v:string) {
  toy.value = v
}

</script>

<style scoped>
</style>

src/components/Son.vue

<template>
<h2>我是子组件Home</h2>
<div>
    子组件的数据: <span>{{ toy }}</span>
</div>
<!-- 2. 定义点击按钮时的回调函数 -->
<button @click="sendToyToFather">把玩具发送给父组件</button>
</template>

<script lang="ts" setup name="Home">
import { ref } from 'vue';

const toy = ref("奥特曼")

// 1. 声明需要触发的父组件中提前定义好的自定义事件
const emit = defineEmits(['sendToy'])

function sendToyToFather() {
    // 3. 触发父组件中定义的自定义事件, 并发携带参数
    emit('sendToy', toy)
}
</script>

3. 通过获取父组件实例操作父组件的数据

src/components/Father.vue

<template>
    <div class="app">
        <h2>我是父组件</h2>
        <h4>房产: {{ houseNum }}</h4>
        <Son1 />
        <Son2 />
    </div>
</template>

<script lang="ts" setup name="Father">
import Son1 from '@/components/Son1.vue';
import Son2 from '@/components/Son2.vue';
import { ref } from 'vue';

let houseNum = ref(4)

// 1. 向外暴露 houseNum
defineExpose({
    houseNum
})
</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/Son1.vue

<template>
  <div class="son">
    <h2>我是儿子1组件</h2>
    <!-- 1. 将 父组件实例 传递给回调函数 -->
    <button @click="changHouseNum($parent)">父亲房产-1</button>
    <h4>玩具: {{ computer }}</h4>
    <h4>书: {{ book }}</h4>
  </div>
</template>

<script lang="ts" setup name="Son">
import { ref } from 'vue';

const computer = ref("苹果笔记本")
let book = ref(6)


function changHouseNum(parent:any){
  // 2. 操作父组件中的数据
  parent.houseNum -= 1
}

</script>

<style scoped>
.son{
    width: 1200px;
    height: 250px;
    border-radius: 20px;
    background-color: cadetblue;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

src/components/Son2.vue

<template>
    <div class="grandson">
        <h2>我是儿子2组件</h2>
        <!-- 1. 将 父组件实例 传递给回调函数 -->
        <button @click="changHouseNum($parent)">父亲房产-1</button>
        <h4>玩具: {{ toy }}</h4>
        <h4>书: {{ book }}</h4>
    </div>
</template>

<script lang="ts" setup name="GrandSon">
import { ref } from 'vue';

const toy = ref("奥特曼")
let book = ref(3)

function changHouseNum(parent:any){
  // 2. 操作父组件中的数据
  parent.houseNum -= 1
}
</script>

<style scoped>
.grandson{
    width: 1200px;
    height: 250px;
    border-radius: 20px;
    background-color: brown;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

3. v-model 父子组件通信原理

UI 组件库的底层大量使用呢 v-model 进行通信

  1. v-model 用在 html 标签上的作用是双向数据绑定
  2. v-model 用在 组件上 的作用是用来通信

1. 双向数据绑定

<template>
	<!-- v-model 的简单写法 -->
    <input type="text" v-model="username">

    <!-- v-model 的底层写法, :value 保证 username 在模版中的渲染, @input 保证在修改的时候同步修改 username-->
    <input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value">
</template>
    
<script lang="ts" setup name="ChildOne">
import { ref } from 'vue';

let username = ref("小明")
</script>

2. UI 组件库中的 input 组件可以写 v-model 来实现双向数据绑定的原理

1. 在页面中使用 UI 组件库

<template>
    <!-- 1. 只在组件上写 v-model 是无法实现双向数据绑定的, 需要在子组件中手动实现一些逻辑 -->
    <MyInput />
    <br>
    
    <!-- 2. 解决办法(底层原理): :modelVlue 来实现页面中的渲染, @update:modelValue(update:modelValue是自定义事件名) 来实现修改 input 中的内容的同时修改 username -->
	<!-- 在 Vue2 中是 :value  @input Vue3 改成了 :modelValue @update:modelValue-->
    <MyInput :modelValue="username" @update:modelValue="username = $event" />

    <!-- 3. 上面解决办法的简单写法 -->
    <MyInput v-model="username"/>
</template>
    
<script lang="ts" setup name="ChildOne">
import { ref } from 'vue';
import MyInput from '@/components/MyInput.vue';

let username = ref("小明")

</script>

2. UI 组件库中已经实现的底层逻辑, 以让使用 UI 组件库的人, 可以很简单的使用 v-model 指令就可以完成双向数据绑定

<template>
    <!-- 3. 绑定 数据 和 自定义事件 -->
    <input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)">
</template>

<script lang="ts" setup name="MyInput">

// 1. 接收父组件传递的数据
defineProps(['modelValue'])

// 2. 接收父组件传递的自定义事件
const emit = defineEmits(["update:modelValue"])

</script>

3. 修改默认名字 (modelValue)

1. 在页面中使用 UI 组件库

<template>
    <!-- 1. v-model:username 是将之前的 modelValue 改为 username, 这样可以传递多个双向数据绑定的数据 -->
    <MyInput v-model:username="username" v-model:password="password"/>
</template>
    
<script lang="ts" setup name="ChildOne">
import { ref } from 'vue';
import MyInput from '@/components/MyInput.vue';

let username = ref("小明")
let password = ref("")

</script>

2. UI 组件库的源码实现逻辑

<template>
    <!-- 3. 绑定 自定义数据 和 自定义事件 -->
    用户名: <input type="text" :value="username" @input="emit('update:username', (<HTMLInputElement>$event.target).value)">
    <br>
    密码: <input type="password" :value="password" @input="emit('update:password', (<HTMLInputElement>$event.target).value)">

</template>

<script lang="ts" setup name="MyInput">

// 1. 接收父组件自定义的数据
defineProps(['username','password'])

// 2. 接收父组件传递的自定义事件
const emit = defineEmits(["update:username","update:password"])

</script>

4. $event 的总结

  1. 对于原生事件中的$event: $event 就是事件对象, 可以 .target
  2. 对于自定义事件的$event: $event 就是触发事件时所传递的数据, 不能 .target

4. solt 插槽 父子组件通信

1. 默认插槽

src/components/Father.vue

<template>
    <div class="app">
        <h3>父组件</h3>
        <div class="items">
            <Son title="游戏排行榜">
                <!-- 1. 将这些标签, 替换子组件的 solt 插槽标签 -->
                <ul>
                    <li v-for="g in games" :key="g.id">{{ g.name }}</li>
                </ul>
            </Son>
            <Son title="美食排行榜"></Son>
            <Son title="电影排行榜"></Son>
        </div>
    </div>
</template>

<script lang="ts" setup name="Father">
import Son from './Son.vue';

let games = [
    {
        id: '001',
        name:"王者荣耀",
        imageUrl: ""
    },
    {
        id: '002',
        name:"英雄联盟",
        imageUrl: ""
    },
    {
        id: '003',
        name:"和平精英",
        imageUrl: ""
    },
]
</script>

<style scoped>
.app{
    width: 100%;
    height: 800px;
    background-color: gray;
}
.items{
    display: flex;
    justify-content: space-around;
}
</style>

src/components/Son.vue

<template>
  <div class="category">
    <h2>{{ title }}</h2>
    <!-- 1. 使用默认插槽 slot 标签占位 -->
    <slot>如果父组件没有传递标签内容, 则显示这些文字内容</slot>
  </div>
</template>

<script lang="ts" setup name="Son">
defineProps(["title"])
</script>

<style scoped>
.category{
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
  padding: 10px;
  width: 200px;
  height: 300px;
  }

  h2 {
    width: 100%;
    height: 40px;
    background-color: orange;
  }
</style>

2. 具名插槽

src/components/Father.vue

<template>
  <div class="app">
    <h3>父组件</h3>
    <div class="items">
      <Son title="游戏排行榜">
        <!-- 1. v-slot 只能放在 组件标签 / template 标签中 -->
        <!-- 2. v-slot:content 会替换组件中 name="content" 的 slot -->
        <template v-slot:content>
          <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ul>
        </template>
        <!-- 2. #title 是 v-slot:title 的简写形式, 它会替换组件中 name="title" 的 slot -->
        <template #title>
          <h2>游戏排行榜</h2>
        </template>
      </Son>
    </div>
  </div>
</template>

<script lang="ts" setup name="Father">
import Son from './Son.vue';
import {reactive} from "vue";

let games = reactive([
  {
    id: '001',
    name: "王者荣耀",
    imageUrl: ""
  },
  {
    id: '002',
    name: "英雄联盟",
    imageUrl: ""
  },
  {
    id: '003',
    name: "和平精英",
    imageUrl: ""
  },
])
</script>

<style scoped>
.app {
  width: 100%;
  height: 800px;
  background-color: gray;
}

.items {
  display: flex;
  justify-content: space-around;
}

h2 {
  width: 100%;
  height: 40px;
  background-color: orange;
}
</style>

src/components/Son.vue

<template>
  <div class="category">
    <!-- 1. 使用具名插槽 slot 标签占位 -->
    <slot name="title">默认标题</slot>
    <slot name="content">默认内容</slot>
  </div>
</template>

<script lang="ts" setup name="Son">
</script>

<style scoped>
.category{
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
  padding: 10px;
  width: 200px;
  height: 300px;
}
</style>

3. 作用域插槽

数据在子组件中, 但是父组件需要拿到子组件中的数据来生成标签结构

1. 默认作用域插槽

src/components/Index.vue

<template>
  <div class="app">
    <h3>父组件</h3>
    <div class="items">
      <!-- 无序列表形式的 Game 组件 -->
      <Game>
        <!-- 1. 使用 s-slot="" 来接收插槽传递过来的数据 -->
        <template v-slot="params">
          <ul>
            <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
          </ul>
        </template>
      </Game>

      <!-- 有序列表形式的 Game 组件 -->
      <Game>
        <!-- 1. 使用 s-slot="" 来接收插槽传递过来的数据, 可以使用 {games} 来解构 -->
        <template v-slot="{games}">
          <ol>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ol>
        </template>
      </Game>
    </div>
  </div>
</template>

<script lang="ts" setup name="Index">
import Game from "@/components/Game.vue";
</script>

<style scoped>
.app {
  width: 100%;
  height: 800px;
  background-color: gray;
}

.items {
  display: flex;
  justify-content: space-around;
}


</style>

src/components/Game.vue

<template>
  <div class="game">
    <h2>游戏排行榜</h2>
    <!-- 1. :games="games", 将子组件的数据传递给插槽的使用者 -->
    <slot :games="games">默认插槽内容</slot>
  </div>
</template>

<script lang="ts" setup name="Game">
import {reactive} from "vue";

let games = reactive([
  {
    id: '001',
    name: "王者荣耀",
    imageUrl: ""
  },
  {
    id: '002',
    name: "英雄联盟",
    imageUrl: ""
  },
  {
    id: '003',
    name: "和平精英",
    imageUrl: ""
  },
])
</script>

<style scoped>
.game{
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
  padding: 10px;
  width: 200px;
  height: 300px;
}
h2 {
  width: 100%;
  height: 40px;
  background-color: orange;
}

</style>

2. 具名作用域插槽

src/components/Index.vue

<template>
  <div class="app">
    <h3>父组件</h3>
    <div class="items">
      <!-- 无序列表形式的 Game 组件 -->
      <Game>
        <!-- 1. 使用 s-slot:具名插槽的名字="" 来接收插槽传递过来的数据 -->
        <template v-slot:game-slot="params">
          <ul>
            <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
          </ul>
        </template>
      </Game>

      <!-- 有序列表形式的 Game 组件 -->
      <Game>
        <!-- 1. 使用 s-slot:具名插槽的名字="", 可以使用 {games} 来解构 -->
        <template v-slot:game-slot="{games}">
          <ol>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ol>
        </template>
      </Game>
    </div>
  </div>
</template>

<script lang="ts" setup name="Index">
import Game from "@/components/Game.vue";
</script>

<style scoped>
.app {
  width: 100%;
  height: 800px;
  background-color: gray;
}

.items {
  display: flex;
  justify-content: space-around;
}


</style>

src/components/Game.vue

<template>
  <div class="game">
    <h2>游戏排行榜</h2>
    <!-- 1. :games="games", 将子组件的数据传递给插槽的使用者 -->
    <slot name="game-slot" :games="games">默认插槽内容</slot>
  </div>
</template>

<script lang="ts" setup name="Game">
import {reactive} from "vue";

let games = reactive([
  {
    id: '001',
    name: "王者荣耀",
    imageUrl: ""
  },
  {
    id: '002',
    name: "英雄联盟",
    imageUrl: ""
  },
  {
    id: '003',
    name: "和平精英",
    imageUrl: ""
  },
])
</script>

<style scoped>
.game{
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
  padding: 10px;
  width: 200px;
  height: 300px;
}
h2 {
  width: 100%;
  height: 40px;
  background-color: orange;
}

</style>

3. 简写形式

src/components/Index.vue

<template>
  <div class="app">
    <h3>父组件</h3>
    <div class="items">
      <!-- 无序列表形式的 Game 组件 -->
      <Game>
        <!-- 1. 使用 #具名插槽的名字="" 来接收插槽传递过来的数据 -->
        <template #game-slot="params">
          <ul>
            <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
          </ul>
        </template>
      </Game>

      <!-- 有序列表形式的 Game 组件 -->
      <Game>
        <!-- 1. 使用 #具名插槽的名字="", 可以使用 {games} 来解构 -->
        <template #game-slot="{games}">
          <ol>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ol>
        </template>
      </Game>
    </div>
  </div>
</template>

<script lang="ts" setup name="Index">
import Game from "@/components/Game.vue";
</script>

<style scoped>
.app {
  width: 100%;
  height: 800px;
  background-color: gray;
}

.items {
  display: flex;
  justify-content: space-around;
}


</style>

src/components/Game.vue

<template>
  <div class="game">
    <h2>游戏排行榜</h2>
    <!-- 1. :games="games", 将子组件的数据传递给插槽的使用者 -->
    <slot name="game-slot" :games="games">默认插槽内容</slot>
  </div>
</template>

<script lang="ts" setup name="Game">
import {reactive} from "vue";

let games = reactive([
  {
    id: '001',
    name: "王者荣耀",
    imageUrl: ""
  },
  {
    id: '002',
    name: "英雄联盟",
    imageUrl: ""
  },
  {
    id: '003',
    name: "和平精英",
    imageUrl: ""
  },
])
</script>

<style scoped>
.game{
  background-color: skyblue;
  border-radius: 10px;
  box-shadow: 0 0 10px;
  padding: 10px;
  width: 200px;
  height: 300px;
}
h2 {
  width: 100%;
  height: 40px;
  background-color: orange;
}

</style>

5. 任意组件通信

1. mitt ( 发布订阅模式 )

1. 下载模块

npm i mitt

**2. 实例化 mitt **

src/utils/emitter.ts

import mitt from "mitt";

const emitter = mitt()
export default emitter

3. 常用方法

// 绑定事件
emitter.on('add',(value)=>{

})

// 触发事件
emitter.emit('add')


// 解绑单个事件
setTimeout(()=>{
    emitter.off('add')
},3000)

// 清空所有事件
setTimeout(()=>{
    emitter.all.clear()
},4000)

5. 示例

需求: 弟弟接收哥哥发送过来的数据

  1. 谁接收数据, 谁绑定事件
  2. 谁发送数据, 谁触发事件

src/components/Father.vue

<template>
  <div class="app">
    <ChildOne />
    <ChildTwo />
  </div>
</template>
  
<script lang="ts" setup name="App">
  import ChildOne from '@/components/ChildOne.vue';
  import ChildTwo from '@/components/ChildTwo.vue';
</script>
  
<style scoped>
</style>

src/components/Child1.vue

<template>
<h2>我是子组件1</h2>
<div>
    子组件的数据: <span>{{ computer }}</span>
</div>
<button>把电脑给父组件</button>
<!-- 2. 定义按钮的点击事件时触发的回调函数 -->
<button @click="sendComputer">把电脑给兄弟组件</button>
</template>

<script lang="ts" setup name="ChildOne">
import { ref } from 'vue';
// 1. 导入 emitter
import emitter from '@/utils/emitter';


const computer = ref("苹果笔记本")

// 3. 定义回调函数
function sendComputer(){
    // 4. 使用 mitt 触发 get-computer 事件, 并传递参数
    emitter.emit('get-computer', computer)
}

</script>

src/components/Child2.vue

<template>
    <h2>我是子组件2</h2>
    <div>
        子组件的数据: <span>{{ toy }}</span>
    </div>
    <div>
        <!-- 3. 使用从兄弟组件传递过来的数据 -->
        接收到来自兄弟组件的数据: <span>{{ computer }}</span>
    </div>
    <button>把玩具给父组件</button>
</template>
    
<script lang="ts" setup name="ChildOne">
import { ref,onUnmounted } from 'vue';
// 1. 导入 emitter
import emitter from '@/utils/emitter';

const toy = ref("奥特曼")
let computer = ref(' ')

// 2. 绑定用来接收数据的 get-computer 事件及回调函数
emitter.on('get-computer',(v:string)=>{
    computer.value = v
})

// 3. 组件卸载时, 需要解绑事件, 释放内存 
onUnmounted(()=>{
    emitter.off('get-computer')
})
</script>

2. pinia

集中式状态 (数据) 管理工具 pinia (类似 vuex)

6. 祖孙组件通信

1. 祖传儿再传孙

1. props

使用 props 一层一层向下传递数据

src/components/Father.vue

<template>
    <div class="app">
        <h2>我是父组件</h2>
        <h4>{{ a }} -- {{ b }} -- {{ c }} -- {{ d }}</h4>
        <!-- 1. 将数据传给儿子组件, 传了四个参数 -->
        <Son :a="a" :b="b" :c="c" :d="d"/>
    </div>
</template>


<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref } from 'vue';

const a = ref(0)
const b = ref(1)
const c = ref(2)
const d = ref(3)


</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/Son.vue

<template>
  <div class="son">
    <h2>我是儿子组件</h2>
    <h4>{{ a }} -- {{ b }}</h4>
    <h4>其他: {{ $attrs }}</h4>
    <!-- 2. 将数据传给孙子组件 -->
    <GrandSon :a="a" :b="b"/>
  </div>
</template>

<script lang="ts" setup name="Son">
import GrandSon from '@/components/GrandSon.vue';

// 1. 接收从父组件传递过来的数据, 只将2个放在props, 其他的被保存在 $attrs 中
defineProps(["a", "b"])
</script>

<style scoped>
.son{
    width: 1200px;
    height: 600px;
    border-radius: 20px;
    background-color: cadetblue;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

src/components/GrandSon.vue

<template>
    <div class="grandson">
        <h2>我是孙子组件</h2>
        <!-- 2. 展示数据 -->
        <h4>{{ a }} -- {{ b }}</h4>
    </div>
</template>

<script lang="ts" setup name="GrandSon">

// 1. 接收从父亲组件传递过来的数据
defineProps(["a", "b"])
</script>

<style scoped>
.grandson{
    width: 1100px;
    height: 400px;
    border-radius: 20px;
    background-color: brown;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

2. $attrs

src/components/Father.vue

<template>
    <div class="app">
        <h2>我是父组件</h2>
        <h4>{{ a }} -- {{ b }} -- {{ c }} -- {{ d }}</h4>
        <!-- 1. 将数据传给儿子组件, 传了四个参数 -->
        <Son :a="a" :b="b" :c="c" :d="d"/>
    </div>
</template>


<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref } from 'vue';

const a = ref(0)
const b = ref(1)
const c = ref(2)
const d = ref(3)


</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/Son.vue

<template>
  <div class="son">
    <h2>我是儿子组件</h2>
    <h4>{{ a }} -- {{ b }}</h4>
    <h4>其他: {{ $attrs }}</h4>
    <!-- 2. 儿子不关心父亲组件传递的数据, 直接全部以 props 的形式传给孙子组件 -->
    <GrandSon v-bind="$attrs"/>
  </div>
</template>

<script lang="ts" setup name="Son">
import GrandSon from '@/components/GrandSon.vue';
    
</script>

<style scoped>
.son{
    width: 1200px;
    height: 600px;
    border-radius: 20px;
    background-color: cadetblue;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

src/components/GrandSon.vue

<template>
    <div class="grandson">
        <h2>我是孙子组件</h2>
        <!-- 2. 展示 props 中的数据 -->
        <h4>{{ a }} -- {{ b }}</h4>
        <!-- 3. 其他数据可以从 $attrs 中看到 -->
        <h4>其他: {{ $attrs }}</h4>
    </div>
</template>

<script lang="ts" setup name="GrandSon">

// 1. 接收从父亲组件传递过来的数据, 存放到 props
defineProps(["a", "b"])
</script>

<style scoped>
.grandson{
    width: 1100px;
    height: 400px;
    border-radius: 20px;
    background-color: brown;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

2. 孙传父再传祖

1. 自定义函数

src/components/Father.vue

<template>
    <div class="app">
        <h2>我是父组件</h2>
        <h4>接收来自孙子组件的数据: {{ defaultData }}</h4>
        <!-- 1. 自定义函数传递给儿子, 再由儿子传递给孙子 -->
        <Son :sendData="saveData"/>
    </div>
</template>


<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref } from 'vue';

let defaultData = ref(0)

// 2. 定义当孙子触发自定义函数时要执行的回调函数
function saveData(value:number){
    defaultData.value = value
}


</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/Son.vue

<template>
  <div class="son">
    <h2>我是儿子组件</h2>
    <!-- 2. 将自定义函数传给孙子组件 -->
    <GrandSon v-bind="$attrs"/>
  </div>
</template>

<script lang="ts" setup name="Son">
import GrandSon from '@/components/GrandSon.vue';

</script>

<style scoped>
.son{
    width: 1200px;
    height: 600px;
    border-radius: 20px;
    background-color: cadetblue;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

src/components/GrandSon.vue

<template>
    <div class="grandson">
        <h2>我是孙子组件</h2>
        <!-- 2. 点击按钮时, 触发 sendData 这个自定义函数, 并将数据传过去 -->
        <button @click="sendData(grandSonData)">点击把数据传给祖宗组件</button>
    </div>
</template>

<script lang="ts" setup name="GrandSon">
import { ref } from 'vue';


let grandSonData = ref(100)
// 1. 接收来自父组件传递的 sendData 自定义函数
defineProps(["sendData"])
</script>

<style scoped>
.grandson{
    width: 1100px;
    height: 400px;
    border-radius: 20px;
    background-color: brown;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

3. 祖直接传孙

src/components/GrandFather.vue

<template>
    <div class="app">
        <h2>我是祖宗组件</h2>
        <h4>金钱: {{ money }} 万</h4>
        <h4>车: 一辆{{ car.brand }}车, 价值{{ car.price }}万</h4>
        <Son />
    </div>
</template>

<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref,provide, reactive } from 'vue';

let money = ref(100)
let car = reactive({
    brand: "奔驰",
    price: 40
})


// 1. 向所有后代提供 数据
provide('money', money)
provide('car', car)

</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/Father.vue

<template>
  <div class="son">
    <h2>我是父亲组件</h2>
    <GrandSon />
  </div>
</template>

<script lang="ts" setup name="Father">
import GrandSon from '@/components/GrandSon.vue';
</script>

<style scoped>
.son{
    width: 1200px;
    height: 500px;
    border-radius: 20px;
    background-color: cadetblue;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>

src/components/GrandSon.vue

<template>
  <div class="grandson">
      <h2>我是孙子组件</h2>
      <h4>金钱: {{ money }} 万</h4>
      <h4>车: 一辆{{ car.brand }}车, 价值{{ car.price }}万</h4>
  </div>
</template>

<script lang="ts" setup name="GrandSon">
import { inject } from 'vue';

// 1. 注入 祖宗组件 提供给后代的 数据 和 函数, 并设置默认值
const money = inject('money')
const car = inject('car',{brand:'未知', price:0})
</script>

<style scoped>
.grandson{
  width: 1100px;
  height: 250px;
  border-radius: 20px;
  background-color: brown;
  margin: 0 auto;
  margin-top: 50px;
}
</style>

4. 孙直接传祖

1. 示例一

祖宗组件中定义一个用于接受数据的自定义函数, 然后提供给后代组件, 孙子组件中触发这个函数, 将数据携带给祖宗组件

src/components/GrandFather.vue

<template>
    <div class="app">
        <h2>我是祖宗组件</h2>
        <h4>玩具: {{ toy }}</h4>
        <Son />
    </div>
</template>

<script lang="ts" setup name="GrandFather">
import Son from '@/components/Son.vue';
import { ref,provide } from 'vue';

let toy = ref('')

// 1. 定义自定义函数, 用来接收孙子组件
function getToy(value:string){
    console.log("获取来自孙子组件的数据: ",value);
    toy.value = value
}

// 2. 向所有后代提供 数据 和 自定义函数
provide('getToy',getToy)

</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/GrandSon.vue

<template>
  <div class="grandson">
      <h2>我是孙子组件</h2>
      <button @click="getToy(toy)">点击发送玩具给祖宗组件</button>
      <h4>玩具: {{ toy }}</h4>
  </div>
</template>

<script lang="ts" setup name="GrandSon">
import { inject, ref } from 'vue';

const toy = ref('奥特曼')
// 1. 注入 祖宗组件 提供给后代的 数据 和 函数, 并设置默认值
const getToy = inject('getToy') as (value:string)=>{}
</script>

<style scoped>
.grandson{
  width: 1100px;
  height: 250px;
  border-radius: 20px;
  background-color: brown;
  margin: 0 auto;
  margin-top: 50px;
}
</style>

2. 示例二

更紧凑的写法

src/components/GrandFather.vue

<template>
    <div class="app">
        <h2>我是祖宗组件</h2>
        <h4>金钱: {{ money }} 万</h4>
        <h4>车: 一辆{{ car.brand }}车, 价值{{ car.price }}万</h4>
        <Son />
    </div>
</template>

<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref,provide, reactive } from 'vue';

let money = ref(100)
let car = reactive({
    brand: "奔驰",
    price: 40
})

function updateMoney(value:number) {
    money.value -= value
}

// 1. 向所有后代提供 数据 和 函数, 函数用来操作祖宗组件内的数据
provide('moneyContext', {money,updateMoney})
provide('car', car)

</script>

<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>

src/components/GrandSon.vue

<template>
    <div class="grandson">
        <h2>我是孙子组件</h2>
        <button @click="updateMoney(10)">修改祖宗的钱</button>
        <h4>金钱: {{ money }} 万</h4>
        <h4>车: 一辆{{ car.brand }}车, 价值{{ car.price }}万</h4>
    </div>
</template>

<script lang="ts" setup name="GrandSon">
import { inject } from 'vue';

// 1. 注入 祖宗组件 提供给后代的 数据 和 函数, 并设置默认值
const {money,updateMoney} = inject('moneyContext', {money:0, updateMoney:(value:number)=>{}})
const car = inject('car',{brand:'未知', price:0})
</script>

<style scoped>
.grandson{
    width: 1100px;
    height: 250px;
    border-radius: 20px;
    background-color: brown;
    margin: 0 auto;
    margin-top: 50px;
  }
</style>
posted @ 2024-08-14 11:02  河图s  阅读(26)  评论(0)    收藏  举报