Vue3学习04 组件通信

Vue3学习04 组件通信

  • 组件通信
    • [props 父 ↔ 子](#props 父 ↔ 子)
    • [自定义事件 子 => 父](#自定义事件 子 => 父)
    • [mitt 任意组件间通信](#mitt 任意组件间通信)
    • [v-model 父↔子](#v-model 父↔子)
    • [attrs 祖↔孙](#attrs 祖↔孙)
    • ```refs\`\`\`、\`\`\`parent```
    • [provide、inject 祖↔孙](#provide、inject 祖↔孙)
    • pinia
    • slot
      • [① 默认插槽](#① 默认插槽)
      • [② 具名插槽](#② 具名插槽)
      • [③ 作用域插槽](#③ 作用域插槽)

组件通信

Vue3组件通信和Vue2的区别:

  • 移除事件总线,使用mitt代替。
  • vuex换成了pinia
  • .sync优化到了v-model里面了。
  • $listeners所有的东西,合并到$attrs中了。
  • $children被砍掉了。

常见搭配形式:

props 父 ↔ 子

概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子

  • 父传子 :属性值是 【非】函数
  • 子传父 :属性值是函数

父组件:

html 复制代码
<template>
  <div class="father">
    <h3>父组件,</h3>
		<h4>我的车:{{ car }}</h4>
		<h4>儿子给的玩具:{{ toy }}</h4>
		<Child :car="car" :getToy="getToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	// 数据
	const car = ref('奔驰')
	const toy = ref()
	// 方法
	function getToy(value:string){
		toy.value = value
	}
</script>

子组件

html 复制代码
<template>
  <div class="child">
    <h3>子组件</h3>
		<h4>我的玩具:{{ toy }}</h4>
		<h4>父给我的车:{{ car }}</h4>
		<button @click="getToy(toy)">玩具给父亲</button>
  </div>
</template>

<script setup lang="ts" name="Child">
	import { ref } from "vue";
	const toy = ref('奥特曼')
	
	defineProps(['car','getToy'])
</script>

自定义事件 子 => 父

  1. 概述:自定义事件常用于:子 => 父。
  2. 注意区分好:原生事件、自定义事件。
  • 原生事件:
    • 事件名是特定的(clickmouseenter等等)
    • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  • 自定义事件:
    • 事件名是任意名称
    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!
  1. 示例:

    html 复制代码
    <!--在父组件中,给子组件绑定自定义事件:-->
    <Child @send-toy="toy = $event"/>
    
    <!--注意区分原生事件与自定义事件中的$event-->
    <button @click="toy = $event">测试</button>
    js 复制代码
    //子组件中,触发事件:
    this.$emit('send-toy', 具体数据)

mitt 任意组件间通信

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

安装mitt

shell 复制代码
npm i mitt

【第一步】:新建文件:src\utils\emitter.ts

javascript 复制代码
// 引入mitt 
import mitt from "mitt";

// 创建emitter
const emitter = mitt()

// emitter.all 拿到所有绑定事件 emitter.all.clear() 清空所有绑定事件
// emitter.emit 触发某一个事件
// emitter.off 解绑某一个时间
// emitter.on  绑定某一个事件

/*
  // 绑定事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被触发',value)
  })
  emitter.on('xyz',(value)=>{
    console.log('xyz事件被触发',value)
  })

  setInterval(() => {
    // 触发事件
    emitter.emit('abc',666)
    emitter.emit('xyz',777)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
*/

// 创建并暴露mitt
export default emitter

【第二步】:接收数据的组件中:绑定事件、同时在销毁前解绑事件:

typescript 复制代码
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on('send-toy',(value)=>{
  console.log('send-toy事件被触发',value)
})

onUnmounted(()=>{
  // 解绑事件
  emitter.off('send-toy')
})

【第三步】:提供数据的组件,在合适的时候触发事件

javascript 复制代码
import emitter from "@/utils/emitter";

function sendToy(){
  // 触发事件
  emitter.emit('send-toy',toy.value)
}

注意这个重要的内置关系,总线依赖着这个内置关系

v-model 父↔子

实际开发中,很少这么写。但是UI组件库中大量使用 v-model。

比如在原生的 input 元素上,使用v-model 可以实现数据动态绑定;使用组件库如elementUI中的 el-input 时候,也可以用v-model实现输入框的数据双向绑定。当我们自定义组件(输入框)时,怎么设置v-model让它实现数据双向绑定

  1. 概述:实现 父↔子 之间相互通信。

  2. 前序知识 ------ v-model的本质

    v-model用在html标签上

    vue 复制代码
    <!-- 使用v-model指令 -->
    <input type="text" v-model="userName">
    
    <!-- v-model的【本质】是下面这行代码 -->
    <input 
      type="text" 
      :value="userName" 
      @input="userName =(<HTMLInputElement>$event.target).value"
           //这里是断言,声明$event.target就是 HTML里面的输入元素
    >
    
    <script>
      let username = ref('zhansgan')
    </script>
  3. 组件标签上的v-model的本质::moldeValueupdate:modelValue事件。

    v-model用在组件标签上

    vue 复制代码
    <!-- 组件标签上使用v-model指令 -->
    <AtguiguInput v-model="userName"/>
    
    <!-- 组件标签上v-model的本质 -->
    <AtguiguInput 
       :modelValue="userName"                  【实现数据到页面】
       @update:model-value="userName = $event" 【实现页面到数据】
                             【 update:model-value被触发后,执行userName=$event】
    />
        <!--在 vue2中,modelValue是value,事件update:model-value是input-->
    事件名:update:model-value
    
    <script>
      let username = ref('zhansgan')
    </script>

    AtguiguInput组件中:

    vue 复制代码
    <template>
      <div class="box">
        <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
    		<!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
        <input 
           type="text" 
           :value="modelValue" 
           @input="emit('update:model-value',$event.target.value)"
        >
         
      </div>
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
      // 接收props
      defineProps(['modelValue'])
      // 声明事件
      const emit = defineEmits(['update:model-value'])
    </script>

    注意事项:上图中 父组件中写的是 e v e n t ,子组件写的是 event,子组件写的是 event,子组件写的是event.target。子组件中是HTML元素,且标准DOM事件,event就是DOM事件对象。但是父组件是组件标签,且是自定义事件,其中的event是传递过来的数据。

    • 如果想更改modelValue、update:model-value的值

    更换value,例如改成abc

    vue 复制代码
    <!-- 也可以更换value,例如改成abc-->
    <AtguiguInput v-model:abc="userName"/>
    
    <!-- 上面代码的本质如下 -->
    <AtguiguInput :abc="userName" @update:abc="userName = $event"/>

    AtguiguInput组件中:

    vue 复制代码
    <template>
      <div class="box">
        <input 
           type="text" 
           :value="abc" 
           @input="emit('update:abc',$event.target.value)"
        >
      </div>
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
      // 接收props
      defineProps(['abc'])
      // 声明事件
      const emit = defineEmits(['update:abc'])
    </script>
  4. 如果value可以更换,那么就可以在组件标签上多次使用v-model

    vue 复制代码
    <AtguiguInput v-model:abc="userName" v-model:xyz="password"/>

$attrs 祖↔孙

  1. 概述:$attrs用于实现当前组件的父组件 ,向当前组件的子组件 通信(祖→孙 )。也可以实现 孙→祖

  2. 具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

    注意:$attrs会自动排除props中声明的属性(可以认为声明过的 props 被子组件自己"消费"了)

父组件:

vue 复制代码
<template>
  <div class="father">
    <h3>父组件</h3>
		<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
      <!--v-bind="{x:100,y:200}"  相当于 :x="100" :y="200"-->
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	let a = ref(1)
	let b = ref(2)
	let c = ref(3)
	let d = ref(4)

    // 传给孙子
	function updateA(value){
		a.value += value
	}
</script>

子组件:

vue 复制代码
<template>
	<div class="child">
		<h3>子组件</h3>
         <!--所有父亲给你但你没有定义的数据,都存在$attrs里面了-->
         <!-- <h1>{{ $attrs }}</h1>  -->
		<GrandChild v-bind="$attrs"/>
	</div>
</template>

<script setup lang="ts" name="Child">
	import GrandChild from './GrandChild.vue'
</script>

孙组件:

vue 复制代码
<template>
	<div class="grand-child">
		<h3>孙组件</h3>
		<h4>a:{{ a }}</h4>
		<h4>b:{{ b }}</h4>
		<h4>c:{{ c }}</h4>
		<h4>d:{{ d }}</h4>
		<h4>x:{{ x }}</h4>
		<h4>y:{{ y }}</h4>
		<button @click="updateA(6)">点我更新A</button>
	</div>
</template>

<script setup lang="ts" name="GrandChild">
	defineProps(['a','b','c','d','x','y','updateA'])
</script>

$refs$parent

  1. 概述:

    • $refs用于 :父→子。
    • $parent用于:子→父。
  2. 原理如下:

    属性 说明
    $refs 值为对象,包含所有被ref属性标识的DOM元素或组件实例。
    $parent 值为对象,当前组件的父组件实例对象。
  • refs示例
  • parent示例

① 父组件向外暴露数据(让数据在子组件中可获取)

② 子组件在函数参数中通过 $parent 获得父组件

输出 $refs

其中子组件的 toy book是ref对象,但是访问(修改)的时候不需要 $refsc1.toy.value 不需要value,因为他被封装在一个响应式对象里面(请看下一节的《ref注意点》)

案例的完整代码

  • Father.vue
vue 复制代码
<template>
	<div class="father">
		<h3>父组件</h3>
		<h4>房产:{{ house }}</h4>
		<button @click="changeToy">修改Child1的玩具</button>
		<button @click="changeComputer">修改Child2的电脑</button>
		<button @click="getAllChild($refs)">让所有孩子的书变多</button>
    <!-- $refs里面包含了所有的儿子 -->
    
		<Child1 ref="c1"/>  <!-- 通过ref获取到孩子1的组件实例对象 -->
		<Child2 ref="c2"/>  <!-- 通过ref获取到孩子2的组件实例对象 -->
	</div>
</template>

<script setup lang="ts" name="Father">
	import Child1 from './Child1.vue'
	import Child2 from './Child2.vue'
	import { ref,reactive } from "vue";
	let c1 = ref()
	let c2 = ref()

	// 数据
	let house = ref(4)
	// 方法
	function changeToy(){
		c1.value.toy = '小猪佩奇'
	}
	function changeComputer(){
		c2.value.computer = '华为'
	}
	function getAllChild(refs:{[key:string]:any}){
    // refs是一个对象  ref:{...}
    // key是字符串,里面存的是any
		console.log(refs)
		for (let key in refs){
      // console.log(key);   C1 C2
      // console.log(refs[key]); c1对象实例、c2对象实例
			refs[key].book += 3
		}
	}
	// 向外部提供数据
	defineExpose({house})

</script>
  • Child1.vue
vue 复制代码
<template>
  <div class="child1">
    <h3>子组件1</h3>
		<h4>玩具:{{ toy }}</h4>
		<h4>书籍:{{ book }} 本</h4>
		<button @click="minusHouse($parent)">干掉父亲的一套房产</button>
  </div>
</template>

<script setup lang="ts" name="Child1">
	import { ref } from "vue";
	// 数据
	let toy = ref('奥特曼')
	let book = ref(3)

	// 方法
	function minusHouse(parent:any){
		parent.house -= 1
	}

	// 把数据交给外部
	defineExpose({toy,book})

</script>
  • Child2.vue
vue 复制代码
<template>
  <div class="child2">
    <h3>子组件2</h3>
		<h4>电脑:{{ computer }}</h4>
		<h4>书籍:{{ book }} 本</h4>
  </div>
</template>

<script setup lang="ts" name="Child2">
		import { ref } from "vue";
		// 数据
		let computer = ref('联想')
		let book = ref(6)
		// 把数据交给外部
		defineExpose({computer,book})
</script>

ref注意点

typescript 复制代码
// 注意点:当访问obj.c的时候,底层会自动读取value属性,因为c是在obj这个响应式对象中的

let obj = reactive({
  a:1,
  b:2,
  c:ref(3)
})

console.log(obj.a) 
console.log(obj.b) 
console.log(obj.c) // c是ref的响应式数据,但是他是放在一个响应式对象里,触碰obj.c的时候,就自动解包了

let x = ref(4)
console.log(x) // 这样就是错的,因为不能自动拆包

provide、inject 祖↔孙

$attrs 实现祖孙通讯打扰了中间人,所以引入本文的provide、inject

  1. 概述:实现祖孙组件直接通信

  2. 具体使用:

    • 在祖先组件中通过provide配置向后代组件提供数据
    • 在后代组件中通过inject配置来声明接收数据
  3. 具体编码:

    【第一步】父组件中,使用provide提供数据

    vue 复制代码
    <template>
      <div class="father">
        <h3>父组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="money += 1">资产+1</button>
        <button @click="car.price += 1">汽车价格+1</button>
        <Child/>
      </div>
    </template>
    
    <script setup lang="ts" name="Father">
      import Child from './Child.vue'
      import { ref,reactive,provide } from "vue";
      // 数据
      let money = ref(100)
      let car = reactive({
        brand:'奔驰',
        price:100
      })
      // 用于更新money的方法
      function updateMoney(value:number){
        money.value += value
      }
      // 提供数据
      provide('moneyContext',{money,updateMoney})
      provide('car',car)
    </script>

    注意:子组件中不用编写任何东西,是不受到任何打扰的

    【第二步】孙组件中使用inject配置项接受数据。

    vue 复制代码
    <template>
      <div class="grand-child">
        <h3>我是孙组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="updateMoney(6)">点我</button>
      </div>
    </template>
    
    <script setup lang="ts" name="GrandChild">
      import { inject } from 'vue';
      // 注入数据
     let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
      let car = inject('car')
    </script>

pinia

参考之前pinia部分的讲解

slot

① 默认插槽

想实现上面的效果:三个有相似的解构,只是内容不同

vue 复制代码
父组件中:
        <Category title="今日热门游戏">
          <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ul>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <!-- 默认插槽 -->
            <slot></slot>
          </div>
        </template>

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

② 具名插槽

vue 复制代码
父组件中:
        <Category title="今日热门游戏">
          <template v-slot:s1>  <!--命名只能放在组件标签或者template中-->
            <ul>
              <li v-for="g in games" :key="g.id">{{ g.name }}</li>
            </ul>
          </template>
          <template v-slot:s2>
            <a href="">更多</a>
          </template>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <slot name="s1"></slot>  <!--s1插槽-->
            <slot name="s2"></slot>  <!--s2插槽-->
          </div>
        </template>

语法糖:父组件中可以不写<template v-slot:s2>,写井号<template #s2>

默认插槽的名字是default <slot name="default"></slot>

③ 作用域插槽

同一份数据,需要不同的展示(顺序、倒叙),用到作用域插槽 。数据在子组件中,但是父组件要对子组件的数据做限制,(压岁钱在孩子那,但根据压岁钱买的东西,由父亲决定)用到作用域插槽。

  1. 理解:数据在组件的自身 (子组件),但根据数据生成的结构需要组件的使用者 (父组件)来决定。(新闻数据在News组件中,但使用数据所遍历出来的结构由App组件决定)

  2. 具体编码:

    vue 复制代码
    父组件中:
          <Game v-slot="params">
          <!-- <Game v-slot:default="params"> -->
          <!-- <Game #default="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>
  • 全部代码

Game.vue

vue 复制代码
<template>
  <div class="game">
    <h2>游戏列表</h2>
    <slot :youxi="games" x="哈哈" y="你好"></slot>
  </div>
</template>

<script setup lang="ts" name="Game">
  import {reactive} from 'vue'
  let games = reactive([
    {id:'asgytdfats01',name:'英雄联盟'},
    {id:'asgytdfats02',name:'王者农药'},
    {id:'asgytdfats03',name:'红色警戒'},
    {id:'asgytdfats04',name:'斗罗大陆'}
  ])
</script>

Father.vue

vue 复制代码
<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
      <Game>
        <template v-slot="params">
          <ul>
            <li v-for="y in params.youxi" :key="y.id">
              {{ y.name }}
            </li>
          </ul>
        </template>
      </Game>

      <Game>
        <template v-slot="params">
          <ol>
            <li v-for="item in params.youxi" :key="item.id">
              {{ item.name }}
            </li>
          </ol>
        </template>
      </Game>

      <Game>
        <template #default="{youxi}">
          <h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3>
        </template>
      </Game>

    </div>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Game from './Game.vue'
</script>
相关推荐
_codeOH3 小时前
Vue 3 vs React 19:框架还在卷,核心原理就这些
前端·vue.js
英勇无比的消炎药4 小时前
新手必看玩转TinyRobot一定要避开这些坑
前端·vue.js
英勇无比的消炎药4 小时前
别再盲目混用AI组件库和传统组件库差距原来这么大
前端·vue.js
英勇无比的消炎药6 小时前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js
英勇无比的消炎药6 小时前
前端提效神器TinyRobot
前端·vue.js
CDwenhuohuo6 小时前
uni 背景色渐变 全屏
前端·javascript·vue.js
爱怪笑的小杰杰6 小时前
Vue 项目交付第三方开发,如何隐藏核心 JS 源码?
前端·javascript·vue.js
小二·7 小时前
Vue 3 组合式 API 进阶实战
前端·javascript·vue.js
rising start8 小时前
九、vue3 组件通信:全场景详解
前端·vue.js·typescript
编程技术手记8 小时前
Vue Scoped CSS 与动态创建 DOM 的兼容性问题
前端·css·vue.js