Vue3---(9)组件间通信

5 阅读5分钟

目录

Props

 自定义事件

mitt

 v-model

$attrs

refsrefs、parent

provide、inject

Pinia

插槽

默认插槽

具名插槽

 作用域插槽

面试重点

为什么不能直接修改 props?如何正确修改?

自定义事件机制原理

自定义事件和全局事件总线如何选择?

 v-model 和 .sync 的区别?

如何处理 v-model 的性能问题?

v-model 在自定义组件中的完整实现流程?​

$attrs 与 props 的区别?

$attrs 包含哪些内容?​ 

为什么说 $parent 是反模式?

如何安全地使用 $refs?​ 

provide/inject 与 Vuex/Pinia 的区别?​

作用域插槽的实现原理?​

Props

使用频率最高的组件间通信方式,一般用来父子组件间进行通信(注意遵循单项数据流原则)

  • 父→子:属性值为非函数,直接传值
  • 子→父:属性值为函数,子组件通过调用父组件方法传递参数 

父组件 

<template>
  <div class="father">
    <h3>父组件,</h3>
		<h4>父亲的车:{{ car }}</h4>
		<h4>儿子给的玩具:{{ toy }}</h4>
         // 父组件通过props直接将属性值传给子组件
		<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>

子组件

<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 接收父组件传递的数据
	defineProps(['car','getToy'])
</script>

 props的详细使用可见之前的文章:props的详细使用****

 自定义事件

自定义事件常用于子组件向父组件传递,子组件通过触发自定义事件,将数据传递给父组件

父组件 

<template>
  <div class="father">
    <h3>父组件</h3>
		<h4 v-show="toy">子给的玩具:{{ toy }}</h4>
		<!-- 给子组件Child绑定自定义事件 -->
    <Child @send-toy="saveToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Child from './Child.vue'
	import { ref } from "vue";
	// 数据
	let toy = ref('')
	// 用于保存传递过来的玩具,接收子组件调用自定义事件传递的参数
	function saveToy(value:string){
		console.log('saveToy',value)
		toy.value = value
	}
</script>

子组件 

<template>
  <div class="child">
    <h3>子组件</h3>
		<h4>玩具:{{ toy }}</h4>
        //触发调用自定义事件,并把toy传递给父组件
		<button @click="emit('send-toy',toy)">测试</button>
  </div>
</template>

<script setup lang="ts" name="Child">
	import { ref } from "vue";
	// 数据
	let toy = ref('奥特曼')
	// 接收父组件传递的自定义事件send-toy
	const emit =  defineEmits(['send-toy'])
</script>

mitt

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

安装

npm i mitt

 引入创建src/utils/mitter.ts

// 引入mitt 
import mitt from "mitt";
// 创建emitter
const emitter = mitt()
  // 绑定事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被触发',value)
  })
  setInterval(() => {
    // 触发事件
    emitter.emit('abc',666)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
// 创建并暴露mitt
export default emitter

 使用注意:要在组件销毁前解绑事件

事件监听: 

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

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

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

事件触发:

import emitter from "@/utils/emitter";

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

 v-model

v-model是Vue的双向绑定语法糖,本质=属性绑定+事件监听的封装

<!-- 基础表单元素 -->
<input v-model="searchText">
<!-- 等价于 -->
当input数据发生改变的时候触发input事件,将改变的值赋值给userName,然后将userName的值用来展示数据
<input 
  type="text" 
  :value="userName" 
  @input="userName =(<HTMLInputElement>$event.target).value"
>

<!-- 自定义组件 -->
<CustomInput v-model="message" />
<!-- 等价于 -->
// 同样的自定义组件的值发生改变时,触发update:modelValue
<CustomInput 
  :modelValue="message" 
  @update:modelValue="message = $event"
>

$attrs

用于透传未声明的props的特性集合,用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙

$attrs是一个对象,包含所有父组件传入的标签属性。 ($attrs会自动排除props中声明的属性 )

父组件

<template>
  <div class="father">
    <h3>父组件</h3>
		<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </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>

 子组件

<template>
	<div class="child">
		<h3>子组件</h3>
		<GrandChild v-bind="$attrs"/>
	</div>
</template>

<script setup lang="ts" 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>
		<h4>d:{{ d }}</h4>
		<h4>x:{{ x }}</h4>
		<h4>y:{{ y }}</h4>
		<button @click="updateA(666)">点我更新A</button>
	</div>
</template>

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

refsrefs、parent

  • $refs用于 :父→子。
  • $parent用于:子→父。
特性$refs$parent
​作用对象​子组件/DOM 元素父组件实例
​数据流向​父组件操作子组件子组件访问父组件
​响应式​非响应式非响应式
​使用场景​需要直接操作子组件/DOM需要访问父组件方法/数据(不推荐)
​Vue3 变化​支持 ref() 组合式 API多根组件中返回首个父组件
​企业级规范​谨慎使用,避免过度依赖几乎禁止使用,推荐替代方案

 父组件

<template>
	<div class="father">
		<h3>父组件</h3>
		<h4>房产:{{ house }}</h4>
		<button @click="changeToy">修改Child1的玩具</button>
		<button @click="changeComputer">修改Child2的电脑</button>
		<button @click="getAllChild($refs)">让所有孩子的书变多</button>
		<Child1 ref="c1"/>
		<Child2 ref="c2"/>
	</div>
</template>

<script setup lang="ts" name="Father">
	import Child1 from './Child1.vue'
	import Child2 from './Child2.vue'
	import { ref,reactive } from "vue";
    //接收ref
	let c1 = ref()
	let c2 = ref()
	// 数据
	let house = ref(4)
	// 通过ref直接改变子组件的值
	function changeToy(){
		c1.value.toy = '小猪佩奇'
	}
	function changeComputer(){
		c2.value.computer = '华为'
	}
	function getAllChild(refs:{[key:string]:any}){
		console.log(refs)
		for (let key in refs){
			refs[key].book += 3
		}
	}
	// 向外部提供数据(只有暴露以后,子组件才能通过$parent来访问到)
	defineExpose({house})
</script>

子组件

<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>

provide、inject

实现祖孙组件间直接通信,解决深层嵌套组件间的​​prop逐层传递​​问题。

provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。

inject()接受两个参数: 第一个参数是注入的 key,第二个参数是可选的,即在没有匹配到 key 时使用的默认值。

  • 在祖先组件中通过provide配置向后代组件提供数据
  • 在后代组件中通过inject配置来声明接收数据

父组件中,使用provide提供数据

<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配置项接受数据

<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的详细使用****

插槽

默认插槽

父组件中:
        <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>

具名插槽

父组件中:
        <Category title="今日热门游戏">
          <template v-slot:s1>
            <ul>
              <li v-for="g in games" :key="g.id">{{ g.name }}</li>
            </ul>
          </template>
          <template #s2>
            <a href="">更多</a>
          </template>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <slot name="s1"></slot>
            <slot name="s2"></slot>
          </div>
        </template>

 作用域插槽

父组件中:
      <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>

面试重点

为什么不能直接修改 props?如何正确修改?

Props 遵循单向数据流原则,直接修改会导致数据流向不清晰,子组件应通过 $emit 触发事件让父组件修改。

自定义事件机制原理

  1. 子组件调用 $emit('event-name', payload)
  2. 父组件通过 @event-name="handler" 监听
  3. Vue 内部维护事件监听器映射表

自定义事件和全局事件总线如何选择?

  • 自定义事件:父子组件直接通信
  • 事件总线:跨组件/兄弟组件通信
  • 复杂场景建议使用 Pinia/Vuex

 v-model 和 .sync 的区别?

  • ​Vue2​​:

    <!-- .sync 实现多个双向绑定 -->
    <Child :title.sync="pageTitle" :content.sync="pageContent" />
    

  • ​Vue3​​:.sync 被废弃,用 v-model:arg 替代实现多个双向绑定

<UserForm v-model:name="name" v-model:age="age" />

如何处理 v-model 的性能问题?

  • 避免深层对象绑定
  • 使用 computed 做代理
  • 对大型列表使用虚拟滚动

v-model 在自定义组件中的完整实现流程?​

  1. 定义 props 接收 modelValue
  2. 定义 emits 声明 update:modelValue
  3. 在需要时调用 emit('update:modelValue', newValue)

$attrs 与 props 的区别?

  • ​​props​​: 显式声明的接收属性
  • ​​$attrs​​: 父组件传递但未声明的剩余属性

$attrs 包含哪些内容?​ 

  • 所有未被声明为 props 的:

    • HTML 属性(idplaceholder 等)
    • 自定义事件(@custom-event
    • class 和 style(Vue3 新增)

为什么说 $parent 是反模式?

  • ​​破坏封装性​​:子组件直接依赖父组件实现细节
  • ​​降低复用性​​:组件无法独立于特定父组件使用
  • ​​维护困难​​:组件层级变化会导致连锁问题

如何安全地使用 $refs?​ 

  • ​​生命周期控制​​:确保在 mounted 后访问
  • ​​类型检查​​:添加必要的空值判断
  • ​​文档注释​​:明确 ref 的用途和接口

provide/inject 与 Vuex/Pinia 的区别?​

  • ​​定位不同​​:

    • provide/inject 是组件级状态共享
    • Vuex/Pinia 是全局状态管理
  • ​​适用场景​​:

    • 跨层级组件通信 → provide/inject
    • 复杂应用全局状态 → Vuex/Pinia

作用域插槽的实现原理?​

  • 子组件将数据通过函数参数传递
  • 父组件接收数据并生成渲染函数
  • 运行时子组件调用该函数并传入数据