sugar__salt头像
关注

Vue 3 组件样式隔离与通信机制:从 scoped 原理到跨组件通信的完整实战


本文基于 Vue 3.5 技术栈,从 scoped 样式隔离的底层原理出发,系统讲解父子组件通信(Props / Emit)、Props 校验、以及 Provide/Inject 和 EventBus 等非父子通信方案。全文"概念 → 原理 → 实战 → 复盘"层层递进,所有代码均可运行。


一、scoped 属性详解

1. 核心作用

在 Vue 单文件组件(.vue)中,<style> 标签默认是全局样式。如果你在 A 组件中写了 h3 { color: red },项目中所有组件的 h3 标签都会变红——这就是样式污染,会造成选择器冲突。

添加 scoped 属性后,样式变为当前组件的局部样式,仅作用于本组件内部 DOM 元素,不会影响外部其他组件。

结合项目代码来直观感受。来看 01-scoped作用及原理 目录下的示例——两个并排的子组件,各自写了 h3 样式:

父组件 App.vue(不写 scoped):

<script setup>
import MyLeft from './components/MyLeft.vue'
import MyRight from './components/MyRight.vue'
</script>

<template>
  <div class="container">
    <my-left />
    <my-right />
  </div>
</template>

<style>
.container {
  display: flex;
}
</style>

左子组件 MyLeft.vue(h3 = 红色):

<script setup></script>

<template>
  <div class="my-left"
    style="flex:1; height:100px; align-items:center; text-align:center; background:paleturquoise;">
    <h3>每天起床第一句</h3>
  </div>
</template>

<style scoped>
h3 {
  color: red;
}
</style>

右子组件 MyRight.vue(h3 = 绿色):

<script setup></script>

<template>
  <div class="my-right"
    style="flex:1; height:100px; align-items:center; text-align:center; background:plum;">
    <h3>先给自己打个气</h3>
  </div>
</template>

<style scoped>
h3 {
  color: green;
}
</style>

运行结果:左侧"每天起床第一句"为红色,右侧"先给自己打个气"为绿色,两者互不干扰。如果去掉 scoped,两个 h3 { color } 规则会互相覆盖,最后只有一个颜色生效。

2. 底层实现原理

scoped 并非浏览器原生能力,而是 Vue 在编译阶段通过 PostCSS 实现的效果。核心机制分两步:

第一步:Vue 自动给当前组件内所有 DOM 标签,添加一个唯一自定义属性 data-v-随机哈希串 例如:

编译前:<h3>每天起床第一句</h3>
编译后:<h3 data-v-7ba5bd90>每天起床第一句</h3>

每个组件的哈希串是全局唯一的——MyLeft 的哈希是 7ba5bd90,MyRight 的哈希是 f3e2d1a0

第二步:编译 CSS 选择器时,自动在每条规则末尾拼接该唯一属性选择器,形成交集选择器。 例如:

/* 你写的源码 */
h3 {
  color: red;
}

/* 编译后实际生效的 CSS */
h3[data-v-7ba5bd90] {
  color: red;
}

原理图解:

组件 MyLeft                         组件 MyRight
     │                                    │
     ▼                                    ▼
 模板编译:每个标签                    模板编译:每个标签
 加 data-v-7ba5bd90                  加 data-v-f3e2d1a0
     │                                    │
     ▼                                    ▼
 CSS 编译:选择器拼接                  CSS 编译:选择器拼接
 h3[data-v-7ba5bd90]                 h3[data-v-f3e2d1a0]
     │                                    │
     ▼                                    ▼
 只匹配左侧 h3 ✅                     只匹配右侧 h3 ✅
 不匹配右侧 h3 ❌                     不匹配左侧 h3 ❌

一句话总结:每个组件拥有自己专属的"身份印章"(data-v-xxx),CSS 选择器只认盖了自己印章的标签。不同组件的印章不同,样式互不干扰——从根源上隔绝了样式污染。

3. 补充特点

scoped 隔离仅对当前组件模板内的标签生效。有一个特殊规则需要注意:

  • 子组件的根节点:会继承父组件的 data-v-xxx 属性,所以父组件的 scoped 样式能在子组件根节点上生效(常用于调整子组件的外边距、定位等)。
  • 子组件内部 DOM(根节点以下的所有元素):不会继承父级哈希属性,父组件的 scoped 样式无法穿透到子组件内部。
<!-- 父组件模板编译后 -->
<div data-v-parent123>
  <ChildComponent data-v-parent123 />  ← 子组件根节点有父级哈希
  <!-- 但 ChildComponent 内部的 <p>、<span> 都没有 data-v-parent123 -->
</div>

二、组件通信总览

组件通信的本质:Vue 各组件各自独立封闭,组件 A 的数据组件 B 天然无法直接访问(就像你的钥匙打不开邻居的门)。因此必须依靠 Vue 提供的指定方案来实现跨组件数据传递。

组件关系与对应通信方案:

组件关系通信方案说明
父传子(核心重点)props 自定义属性最基础的通信方式
子传父(核心重点)defineEmits 自定义事件子通知父修改数据
隔代祖先与后代provide + inject跨越多层中间组件
兄弟 / 无关联跨组件EventBus 事件总线借助公共模块中转
大型项目全局跨组件Pinia 状态管理库推荐用于复杂项目

本文重点覆盖前四种方案,Pinia 作为大型项目终极方案,后续会单独深入学习。

如何判定父子组件? 规则非常简单——在 A 组件的模板中(<template>),以自定义标签的形式引入 B 组件,则:A 为父组件,B 为子组件

<!-- App.vue -->
<template>
  <MyGoods />   <!-- App 是父,MyGoods 是子 -->
</template>

三、父传子(Props)详解

1. 使用场景

子组件的内容不能写死为固定数据。比如一个商品卡片组件,商品名称、价格、图片每次都不同——这些数据需要由父组件动态传入,这样才能做到"一个组件,复用 N 次",提升组件的复用性与灵活性。

2. 两步流程(口诀:子接父传)

第一步:父组件(提供数据)——在子组件标签上,通过 :自定义属性名="父组件数据" 绑定动态属性,也就是 props。

第二步:子组件(接收数据)——在 <script setup> 中使用 defineProps() 显式声明接收的属性,接收后才可在模板与逻辑中使用。

完整代码示例——商品卡片:

父组件 App.vue

<script setup>
// 导入 MyGoods 商品组件
import MyGoods from './components/MyGoods.vue'

// 父组件提供数据
const goodsObj = {
  id: '3998274',
  name: '德国百年工艺高端水晶玻璃红酒杯2支装',
  price: 139,
  picture: 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg'
}
</script>

<template>
  <div class="list">
    <!-- ★ 父传:子组件标签上通过 :自定义属性 绑定数据 -->
    <MyGoods
      :imgUrl="goodsObj.picture"
      :title="goodsObj.name"
      :price="goodsObj.price"
    />
  </div>
</template>

<style lang="scss">
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.list {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
}
</style>

子组件 MyGoods.vue

<script setup>
// ★ 子接:defineProps() 声明接收的属性,之后即可在模板中使用
defineProps(['imgUrl', 'title', 'price'])
</script>

<template>
  <div class="item">
    <img :src="imgUrl" />
    <p class="name">{{ title }}</p>
    <p class="price">{{ price }}.00</p>
  </div>
</template>

<style scoped lang="scss">
.item {
  width: 240px;
  margin-left: 10px;
  padding: 20px 30px;
  transition: all 0.5s;
  margin-bottom: 20px;

  // 第 4n 个清除左边距
  .item:nth-child(4n) {
    margin-left: 0;
  }

  &:hover {
    box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
    transform: translate3d(0, -4px, 0);
    cursor: pointer;
  }

  img {
    width: 100%;
  }

  .name {
    font-size: 18px;
    margin-bottom: 10px;
    color: #666;
  }

  .price {
    font-size: 22px;
    color: firebrick;

    &::before {
      content: '¥';
      font-size: 22px;
    }
  }
}
</style>

配合 v-for 循环渲染列表是 Props 最经典的实战场景。父组件维护一个商品列表数组,通过 v-for 循环将每一项数据逐个传给子组件:

<script setup>
import MyGoods from './components/MyGoods.vue'

// 商品列表 — 8 件商品
const goodsList = [
  { id: '4001172', name: '称心如意手摇咖啡磨豆机咖啡豆研磨机', price: 289,
    picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg' },
  { id: '4001594', name: '日式黑陶功夫茶组双侧把茶具礼盒装', price: 288,
    picture: 'https://yanxuan-item.nosdn.127.net/3346b7b92f9563c7a7e24c7ead883f18.jpg' },
  { id: '4001009', name: '竹制干泡茶盘正方形沥水茶台品茶盘', price: 109,
    picture: 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png' },
  { id: '4001874', name: '古法温酒汝瓷酒具套装白酒杯莲花温酒器', price: 488,
    picture: 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png' },
  { id: '4001649', name: '大师监制龙泉青瓷茶叶罐', price: 139,
    picture: 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png' },
  { id: '3997185', name: '与众不同的口感汝瓷白酒杯套组1壶4杯', price: 108,
    picture: 'https://yanxuan-item.nosdn.127.net/8e21c794dfd3a4e8573273ddae50bce2.jpg' },
  { id: '3997403', name: '手工吹制更厚实白酒杯壶套装6壶6杯', price: 100,
    picture: 'https://yanxuan-item.nosdn.127.net/af2371a65f60bce152a61fc22745ff3f.jpg' },
  { id: '3998274', name: '德国百年工艺高端水晶玻璃红酒杯2支装', price: 139,
    picture: 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg' }
]
</script>

<template>
  <div class="list">
    <!-- 自定义组件配合 v-for 循环使用,每次迭代把 item 字段传给子组件 -->
    <MyGoods
      v-for="item in goodsList"
      :key="item.id"
      :imgUrl="item.picture"
      :title="item.name"
      :price="item.price"
    />
  </div>
</template>

运行效果:1000px 宽容器内,8 张商品卡片自动排列(每行 4 个,共 2 行),每张卡片独立显示各自的图片、名称和价格。

3. 关键特性:单向数据流

Props 属于只读数据:数据的"所有权"属于父组件,子组件只能读取展示,不允许直接修改 props 变量的值。如果子组件尝试 props.price = 200,Vue 会在控制台抛出警告:

[Vue warn] Set operation on key "price" failed: target is readonly.

设计理念:数据只有一个入口(父组件传入)和一个出口(父组件修改)。如果允许子组件随意修改父组件的数据,数据流向将变得不可追踪,调试成本会爆炸式增长。这是 Vue 单向数据流的核心约束。


四、子传父(自定义事件 emit)详解

1. 使用场景

子组件需要"反向"修改父组件的数据。但 Props 是只读的,子组件不能直接动手改。于是通过自定义事件,子组件通知父组件:“数据该改了,你来执行修改逻辑”。

2. 两步流程(口诀:父监听,子触发)

第一步:父组件(监听事件)——在子组件标签上绑定自定义事件:@自定义事件名="父组件处理函数"

第二步:子组件(触发事件)——使用 defineEmits 声明自定义事件,在合适时机调用 emit('事件名', 需要回传给父组件的参数),触发父组件绑定的函数,同时携带数据。

举例场景——砍价功能:子组件有一个"砍一刀"按钮,点击后需要减少父组件中对应商品的价格。Props 中的价格是只读的,子组件不能直接改,于是触发 cutPrice 事件通知父组件去改。

父组件 App.vue

<script setup>
import { ref } from 'vue'
import MyGoods from './components/MyGoods.vue'

// 商品列表
const goodsList = ref([
  { id: '4001172', name: '称心如意手摇咖啡磨豆机咖啡豆研磨机', price: 289,
    picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg' },
  { id: '4001594', name: '日式黑陶功夫茶组双侧把茶具礼盒装', price: 288,
    picture: 'https://yanxuan-item.nosdn.127.net/3346b7b92f9563c7a7e24c7ead883f18.jpg' },
  // ... 共 8 件商品
  { id: '3998274', name: '德国百年工艺高端水晶玻璃红酒杯2支装', price: 139,
    picture: 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg' }
])

// i: 下标,指定让哪个对象的 price 减少
// price: 每次减少的价格
const subPrice = (i, price) => {
  goodsList.value[i].price -= price
}
</script>

<template>
  <div class="list">
    <MyGoods
      v-for="(item, index) in goodsList"
      :key="item.id"
      :imgUrl="item.picture"
      :title="item.name"
      :price="item.price"
      :idx="index"
      @ccc="subPrice"         <!--  父监听:绑定自定义事件 @ccc -->
    />
  </div>
</template>

子组件 MyGoods.vue

<script setup>
import { ref } from 'vue'

// 子传父的语法(口诀):2 步
// 1. 父组件内,子组件的自定义标签上,绑定自定义事件,@自定义事件="父修改数据的函数"(父监听/绑定)
// 2. 子组件在恰当时机,触发自定义事件,导致父组件修改数据函数的执行,
//    emit('自定义事件', 携带的参数...) (子触发/通知)

const props = defineProps(['imgUrl', 'title', 'price', 'idx'])

// 拿到触发自定义事件的函数 emit
const emit = defineEmits()

// 子组件自己的数据:每次砍价减少的金额
const x = ref(3)

// 点击子组件砍价按钮
const onCut = () => {
  // ★ 子触发:emit('事件名', 回传给父组件的参数...)
  emit('ccc', props.idx, x.value)
}
</script>

<template>
  <div class="item">
    <img :src="imgUrl" />
    <p class="name">{{ title }}</p>
    <p class="price">
      <span>{{ price }}.00</span>
      <button @click="onCut">砍一刀</button>
    </p>
  </div>
</template>

<style scoped lang="scss">
.item {
  width: 240px;
  margin-left: 10px;
  padding: 20px 30px;
  transition: all 0.5s;
  margin-bottom: 20px;

  .item:nth-child(4n) {
    margin-left: 0;
  }

  &:hover {
    box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
    transform: translate3d(0, -4px, 0);
    cursor: pointer;
  }

  img {
    width: 100%;
  }

  .name {
    font-size: 18px;
    margin-bottom: 10px;
    color: #666;
  }

  .price {
    display: flex;
    align-items: center;
    height: 36px;
    font-size: 22px;
    color: firebrick;

    button {
      margin-left: 50px;
      font-size: 14px;
    }

    &::before {
      content: '¥';
      font-size: 22px;
    }
  }
}
</style>

数据流追踪:

用户点击子组件 [砍一刀]
        │
        ▼
子组件 onCut() 执行
        │
        ▼
emit('ccc', props.idx, x.value)    ← 携带参数:商品下标 + 砍价金额
        │
        ▼
父组件 @ccc="subPrice" 监听到事件
        │
        ▼
subPrice(商品下标, 砍价金额) 执行
        │
        ▼
goodsList.value[i].price -= 砍价金额   ← 父组件修改自己的数据
        │
        ▼
响应式更新 → 页面价格自动刷新 ✅

核心理解:子组件不做修改,只做"通知"——它告诉父组件"有人对第 N 个商品砍了 M 元"。父组件收到通知后,自己动手修改自己的数据。整个过程中,数据的所有权始终在父组件手里。


五、Props 校验

1. 目的

约束父组件传入数据的类型、必填项、默认值、自定义规则。当父组件传错参数时,控制台会给出明确警告,保障代码健壮性,把 Bug 扼杀在开发阶段。

2. 两种写法

① 简易写法(仅约束类型)
const props = defineProps({
  opacity: Number,
  list: Array
})

多个合法类型可以写成数组:

opacity: [Number, String]
② 完整高阶写法(四项配置)
const props = defineProps({
  opacity: {
    type: Number,         // ① 限定数据类型
    default: 0.5,         // ② 不传时的默认值
    required: true,       // ③ 是否必填
    validator(value) {    // ④ 自定义校验函数,返回布尔值
      return value >= 0 && value <= 1
    }
  }
})

⚠️ 注意defaultrequired 互斥,二选一使用。如果设了 required: true,说明父组件一定会传值,默认值就没有意义了。

另外,引用类型(数组/对象)的 default 必须写成函数返回的形式,否则所有组件实例会共享同一个引用:

// ❌ 错误 — 所有实例共享同一个数组引用
list: {
  type: Array,
  default: []
}

// ✅ 正确 — 每个实例拿到独立的新数组
list: {
  type: Array,
  default: () => []
}

// ✅ 对象同理
obj: {
  type: Object,
  default: () => ({})
}

六、props 与组件内部 ref/reactive 的核心区别

很多初学者会把 Props 和组件内部的 ref/reactive 混为一谈。它们虽然都是响应式数据,但本质完全不同

维度props 数据ref / reactive 组件内部响应式数据
数据来源父组件传入,外部数据组件内部自己声明创建
读写权限只读,子组件不可直接修改可读可写,组件完全自主控制
数据流严格单向:仅父 → 子完全内部闭环,不受单向数据流限制

核心总结一句话:谁的数据,谁负责。

  • 父组件的数据,只能由父组件自己修改。子组件想改?只能 emit 通知父组件。
  • 组件自身创建的数据(ref / reactive),自己想怎么改就怎么改,完全自主控制。
<script setup>
import { ref } from 'vue'

// props — 父组件的数据,只读
const props = defineProps(['price'])
// props.price = 100  ← ❌ 报错!不能改

// ref — 组件自己的数据,随便改
const cutAmount = ref(3)
cutAmount.value = 5    // ← ✅ 可以改,数据是我自己创建的
</script>

七、非父子组件通信方案

1. 祖先组件 → 深层后代组件:provide / inject

使用场景:数据要从"爷爷"组件直接传给"孙子"组件,中间隔着一层或多层父组件。如果用 Props,每一层都要手动传递,中间的父组件被迫接收自己不需要的数据——这叫 Props 逐层透传(Prop Drilling)。

方案

  • 祖先组件provide('key', 要传递的数据) 向外暴露数据;
  • 任意后代组件const data = inject('key') 直接获取,不受层级深度限制

完整示例——爷爷给孙子留"小目标":

祖先组件(App.vue,爷爷):

<script setup>
import { provide, ref } from 'vue'
import MyParent from './components/MyParent.vue'

// 爷爷给孙子留下的小目标
const money = ref(100000000)

// ★ 爷爷提供的数据
provide('qian', money)
</script>

<template>
  <div class="app">
    <h3>App组件</h3>
    <MyParent />
  </div>
</template>

<style scoped>
.app {
  width: 400px;
  padding: 20px;
  background: paleturquoise;
}
</style>

中间组件(MyParent.vue,父亲):

<script setup>
import MyChild from './MyChild.vue'
</script>

<template>
  <div class="my-parent">
    <h3>MyParent组件</h3>
    <MyChild />
  </div>
</template>

<style scoped>
.my-parent {
  padding: 20px;
  background: plum;
}
</style>

后代组件(MyChild.vue,孙子):

<script setup>
import { inject } from 'vue'

// ★ 获取爷爷给的小目标 — 直接 inject,无需经过父组件
const money = inject('qian')
</script>

<template>
  <div class="my-child">
    <h3>MyChild组件</h3>
    <p>收到来自爷爷的小目标 = {{ money }}</p>
  </div>
</template>

<style scoped>
.my-child {
  padding: 20px;
  background: palegoldenrod;
}
</style>

组件层级与数据流动:

App.vue(爷爷)
  │  provide('qian', 100000000)
  │
  └─▶ MyParent.vue(父亲)
        │  ← 不需要写任何 props 传递代码!
        │
        └─▶ MyChild.vue(孙子)
              │  inject('qian')  →  100000000 ✅
              │
              └─ "收到来自爷爷的小目标 = 100000000"

弊端:数据响应式追踪较弱,不适合高频复杂数据场景。数据来源不如 props 直观,后续维护时需要在组件树中搜索 provide 的出处。

2. 任意无关联组件(兄弟组件为主):EventBus 事件总线

原理:创建独立公共 JS 模块作为事件中间媒介,组件双方都引入。发送方 emit 发射,接收方 on 监听——双方互不感知。

使用 mitt 库(Vue 3 官方推荐,替代已废弃的 $on/$emit)。

eventBus.js:

import mitt from 'mitt'
const meipo = mitt()      // 创建中间人
export default meipo

发送方 MyLeft.vue(牛郎):

<script setup>
import { ref } from 'vue'
import meipo from '../eventBus.js'
const msg = ref('两情若是长久时, 又岂在朝朝暮暮')
const word = ref('')
const onSend = () => { meipo.emit('biubiubiu', msg.value) }    // ★ 发送
meipo.on('ZhaNan', (str) => { word.value = str })              // 监听回话
</script>
<template>
  <div class="my-left"><h3>牛郎</h3>
  <button @click="onSend">Send</button>
  <p>回话={{ word }}</p></div>
</template>

接收方 MyRight.vue(织女):

<script setup>
import { ref } from 'vue'
import meipo from '../eventBus.js'
const str = ref('')
meipo.on('biubiubiu', (msg) => {      // ★ 接收
  str.value = msg
  meipo.emit('ZhaNan', '渣男')       // 回复
})
</script>
<template>
  <div class="my-right"><h3>织女</h3><p>消息={{ str }}</p></div>
</template>

通信流程: 牛郎 emit('biubiubiu') → 织女 on('biubiubiu') 收到 → 织女 emit('ZhaNan') → 牛郎 on('ZhaNan') 收到。双方通过 meipo 中转,互不感知。

┌─────────────────┐              ┌─────────────────┐
│   MyLeft(牛郎) │              │  MyRight(织女) │
├─────────────────┤              ├─────────────────┤
│                 │  emit('biubiubiu', msg)         │
│ 点击 Send ──────┼──────┐  ┌─────┼── on('biubiubiu')
│                 │      │  │     │  收到消息        │
│                 │      ▼  ▼     │                 │
│                 │  ┌──────────┐ │                 │
│                 │  │  meipo   │ │                 │
│                 │  │ (EventBus)│ │                 │
│                 │  └──────────┘ │                 │
│                 │      │  │     │                 │
│ on('ZhaNan') ◀──┼─────┘  └─────┼── emit('ZhaNan','渣男')
│ 收到回话 ✅      │              │ 回复消息          │
└─────────────────┘              └─────────────────┘

适合中小型简单跨组件通信。大型项目推荐 Pinia 全局状态库,因为 EventBus 在复杂场景下会变成"事件地狱"——事件名满天飞,数据流向难以追踪和调试。


全文总结

本文严格按"样式隔离 → 组件关系 → 父子通信 → 数据校验 → 跨级/兄弟通信"的递进路径,覆盖了 Vue 3 组件化开发的核心知识体系:

  1. scoped 样式隔离:通过编译时给 DOM 添加唯一 data-v-xxx 属性 + CSS 选择器自动拼接属性选择器,从根源上解决样式污染。理解这个机制是排查样式相关 Bug 的基础。
  2. Props 父传子:组件复用的基石,遵循"子接父传"两步走 + 单向数据流铁律,配合 v-for 实现列表循环渲染。
  3. Emit 子传父:突破 Props 只读限制的正确途径,本质是"通知"而非"修改",数据所有权始终在父组件手中。
  4. Props 校验:从简易类型约束到完整四件套配置(type / default / required / validator),是代码健壮性的重要防线。引用类型的 default 必须用函数返回。
  5. Provide / Inject:解决深层后代数据传递的利器,一次 provide,所有后代直接 inject,不受层级限制。
  6. EventBus(mitt):兄弟组件/无关联组件通信的轻量方案,通过公共 JS 模块作为事件中转站,emit 发送、on 接收。

核心知识点复盘

知识点核心要点
scoped 原理编译时:① 每个 DOM 标签加 data-v-hash 属性;② CSS 选择器拼接 [data-v-hash],形成交集选择器,样式只匹配本组件
scoped 渗透规则子组件根节点会继承父级哈希(父 scoped 样式可影响子组件根节点),但子组件内部 DOM 不会继承
父子组件判定A 组件的 <template> 中以自定义标签引入 B → A 是父,B 是子
父传子(Props)父::自定义属性="数据" 绑定;子:defineProps(['属性名']) 接收;口诀"父传子接"
单向数据流Props 只读,子组件不能直接修改;数据所有权属于父组件,修改只能由父组件执行
子传父(Emit)父:@自定义事件="处理函数" 监听;子:defineEmits() + emit('事件', 参数) 触发;口诀"父监听子触发"
Props 简易校验defineProps({ opacity: Number }),仅约束类型;多类型用数组 [Number, String]
Props 完整校验四项:type(类型)、default(默认值)、required(必填)、validator(value)(自定义校验,返回布尔值)
引用类型 default 坑数组/对象默认值必须用函数 default: () => [],否则所有实例共享同一引用导致互相影响
Provide / Inject祖先 provide('key', data),后代 inject('key') 获取;跨多层,中间组件无需参与
EventBus(mitt)独立 JS 公共模块作为事件中间人;emit 发送、on 接收;适合中小型跨组件通信,大型项目推荐 Pinia
Props vs ref/reactiveProps = 外部传入只读,ref/reactive = 内部创建可读写;核心原则"谁的数据谁负责"

常见问题 / 避坑指南

Q1:子组件不小心修改了 Props 中的对象属性,为什么有时不报错?

当你写 props.obj.name = 'xxx' 时,修改的是对象内部属性,不是 props 引用本身,Vue 可能不会立即警告。但这依然是错误做法,会破坏单向数据流,埋下难以追踪的 Bug。正确做法永远是 emit 通知父组件修改。

Q2:defineProps 和 defineEmits 需要 import 吗?

不需要。这两个是 Vue 的编译宏(Compiler Macro),在 <script setup> 中可以直接使用,Vue 编译器自动处理。手动 import 反而会报错。

Q3:EventBus 中组件销毁后,监听器还在吗?

依然在内存中!需要手动在组件卸载时调用 meipo.off('事件名', 处理函数) 取消监听,否则可能导致内存泄漏。使用 onUnmounted 钩子处理:

import { onUnmounted } from 'vue'

const handler = (data) => { /* ... */ }
meipo.on('someEvent', handler)

onUnmounted(() => {
  meipo.off('someEvent', handler)
})

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/suger__salt/article/details/163279406

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--