首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一文掌握Vue3的组件&组件通信

一文掌握Vue3的组件&组件通信

作者头像
方才编程_公众号同名
发布2024-11-19 13:22:40
发布2024-11-19 13:22:40
5560
举报
文章被收录于专栏:方才编程方才编程

本文转载自专栏《1小时掌握vue3》:https://fangcaicoding.cn/course/12/66

大家好!我是方才,目前是8人后端研发团队的负责人,拥有6年后端经验&3年团队管理经验。 系统学习践行者!近期在系统化输出前端入门相关技术文章,期望能帮大家构建一个完整的知识体系。 如果对你有所帮助,记得一键三连! 我创建了一个编程学习交流群(扫码关注后即可加入),秉持“一群人可以走得更远”的理念,期待与你一起 From Zero To Hero! 茫茫人海,遇见即是缘分!方才兄送你ElasticSearch系列知识图谱、前端入门系列知识图谱、系统架构师备考资料!

Hello,大家好!我是方才。今天我们开始学习vue的组件化。

在前端开发中,组件化就是“搭积木”——将页面功能拆分成一个个可复用的“积木块”,然后自由组合,快速搭建出各种炫酷的页面。

image-20241117210729947

什么是组件

简单来说,组件就是一段封装好的代码,负责一部分独立的功能。

比如一个留言板页面,你的评论框、评论列表、点赞按钮都可以是组件。组件最大的好处是“复用性”和“独立性”,换句话说,写一次,用十次,bug不会传染,收益直接翻倍!

就以方才兄的博客系统为例,页面的基础布局就是一个通用组件,所有页面都在使用:

image-20241117171400559

再比如说文章内容页也是一个组件,单文章详情和教程文章详情复用了该组件,统一实现文章内容的渲染和登录限制逻辑:

image-20241117172136289

组件关系

image-20241117173456816

组件关系就像家庭关系:

  • 父组件好比一家之主,掌握全局;
  • 子组件负责自己的“小地盘”,但听从父组件的安排;
  • 兄弟组件偶尔吵架,需要借助“和事佬”沟通。

ps:对于初学者,可能无法区分子组件和父组件。方才兄这里白话文解释下:谁被使用,谁就是子组件,使用者相对而言就是父组件。

如何定义和使用组件

简单来个demo,完成组件的定义和使用,顺便去理解组件关系:

  • 定义孙组件GrandSonDemo.vue
代码语言:javascript
复制
<template>
 <h3>这是孙组件</h3>
</template>

<script setup>

</script>

<style scoped>

</style>
  • 定义子组件SonDemo.vue(子组件2 Son2Demo的代码实现类似):
    • SonDemo.vue使用import关键字导入了孙组件,并在模板<template>中使用了孙组件。
代码语言:javascript
复制
<template>
 <h2>这是子组件</h2>
  <grand-son-demo/>
</template>

<script setup>

import GrandSonDemo from "./GrandSonDemo.vue";
</script>


<style scoped>

</style>
  • 因为目前还没有学习路由,方便看允许效果,我们先将App.vue作为父组件:
    • App.vue使用import关键字导入了子组件,并在模板<template>中使用了子组件。
代码语言:javascript
复制
<template>
  <h1>这是父组件</h1>
  <SonDemo/>
  <Son2Demo/>
</template>


<script setup>
import SonDemo from "./components/SonDemo.vue";
import Son2Demo from "./components/Son2Demo.vue";
</script>

<style scoped>

</style>

运行效果:

image-20241117180422349

这就完成了组件的定义和使用。组件之前没有内容的联动和通信,接下来,我们就开始父子组件的联动和通信。

插槽 slot:灵活扩展的秘诀

插槽是组件间内容分发的“百宝箱”。你可以把插槽理解为子组件给父组件留的一块空地,父组件可以随时填充内容。

注意填充的内容:可以是纯文本,也可以是html代码块(也成为模板内容)。

接下来方才兄结合博客系统的部分实现为例,带大家理解下插槽的使用。

默认插槽

我们在SonDemo.vue的基础上进行修改,增加一个广告栏的插槽:

  • <slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
  • <slot> 元素内的内容,在外部没有提供任何内容的情况下,就是该插槽的默认内容。
  • 注意:这里我们并没有给<slot> 元素设置一个名字,vue会给一个默认值,等价于<slot name="default">
代码语言:javascript
复制
<template>
  <h2>这是子组件</h2>
  <grand-son-demo/>

  <div class="slot_demo_class">
    <slot>
      <p>这是默认插,这是一个广告栏</p>
    </slot>
  </div>
</template>

<script setup>

import GrandSonDemo from "./GrandSonDemo.vue";
</script>


<style scoped>
.slot_demo_class {
  color: red;
}
</style>

就好比:买房时,开发商说“这里是客厅,怎么装修你随意!”

image-20241117182704697

插槽内容赋值

现在,我们需要去“装修”这个广告栏了,只需要在父组件App.vue中,传递想要的内容即可:

代码语言:javascript
复制
<template>
  <h1>这是父组件</h1>
  <SonDemo>
    父组件做了个简单装修的广告栏
  </SonDemo>
</template>

<script setup>
import SonDemo from "./components/SonDemo.vue";
</script>

<style scoped>

</style>

image-20241117182752025

具名插槽

具名插槽:就是有指定名称的插槽。使用场景是,在同一个组件中提供多个插槽,需要有名称做区分,比如 <slot name="left_slogan_slot">

还是接着上面的例子,SonDemo.vue中有两个广告位,一个左侧广告位,一个右侧广告位。

代码语言:javascript
复制
<template>
  <h2>这是子组件</h2>
  <grand-son-demo/>

  <div class="slot_demo_class">
    <div class="slogan_slot">
      <slot name="left_slogan_slot">
        <p>这是默认插槽,这是一个左侧广告栏</p>
      </slot>
    </div>
    <div class="slogan_slot">
      <slot name="right_slogan_slot">
        <p>这是另一个默认插槽,这是一个右侧的广告栏</p>
      </slot>
    </div>
  </div>
</template>

<script setup>

import GrandSonDemo from "./GrandSonDemo.vue";
</script>


<style scoped>
.slot_demo_class {
  color: red;
  display: flex;
}
.slogan_slot {
  /* 每个插槽区域平分剩余空间 */
  flex: 1;
}
</style>

App.vue通过插槽名称,精准装修不同的“广告位”:

  • 为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令,示例 <template v-slot:right_slogan_slot> xxx </template>
  • v-slot 有对应的简写 #,示例 <template #left_slogan_slot> xxx </template>
代码语言:javascript
复制
<template>
  <h1>这是父组件</h1>
  <SonDemo>
    <template #left_slogan_slot>
      这是被装修后的左侧广告栏
    </template>
    <template v-slot:right_slogan_slot>
      这是被装修后的右侧广告栏
    </template>
  </SonDemo>
</template>


<script setup>
import SonDemo from "./components/SonDemo.vue";
</script>

<style scoped>

</style>

效果如下:

image-20241117192923339

作用域插槽

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。这就是作用域插槽。

子组件通过插槽把数据“扔”给父组件,父组件灵活展示。接着上面的例子来:

  • SonDemo.vue,再增加一个插槽,传递参数:tempInt="123":
代码语言:javascript
复制
    <div class="slogan_slot">
      <slot name="temp_slogan_slot" :tempInt="123">
        <p>这是临时插槽,但我可以传递参数给父组件</p>
      </slot>
    </div>
  • App.vue中接受参数:
代码语言:javascript
复制
  <template #temp_slogan_slot="param">
      这是被装修后的临时插件,子组件传递的参数为 {{ param.tempInt }}
   </template>

效果:

image-20241117194745761

defineProps:父传子通信神器

父组件如何把信息“打包”送到子组件?子组件中使用 defineProps接收父组件向子组件传递的属性(props)。

比如说,博客文章详情页的组件BaseArticle.vue,需要由父组件传递文章信息:

代码语言:javascript
复制
<template>

  <h1>{{ articleProps.title }}</h1>
  <h4>ID: {{ articleProps.id }}</h4>
  <p>Author: {{ articleProps.author }}</p>
  <p>Content: {{ articleProps.content }}</p>
</template>

<script setup>
const articleProps = defineProps({
  // 定义id字段,且约定必须传,类型为Number
  id: {
    type: Number,
    required: true
  },
  // 定义title字段,且约定必须传,类型为String,默认值为'default title'
  title: {
    type: String,
    required: true,
    default: 'default title'
  },
  // 定义author字段,且约定必须传,类型为String
  author: {
    type: String,
    required: true
  },
  // 定义content字段,不做任何约束,类型为任意类型
  content: {}
})
</script>

<style scoped>

</style>

在父组件App.vue中传递响应式数据:

代码语言:javascript
复制
<template>
  <h1>这是父组件</h1>
  <BaseArticle :id="article.id" :title="article.title" :content="article.content" :author="article.author" />
</template>

<script setup>
import BaseArticle from "./components/BaseArticle.vue";
import {ref} from "vue";

const article = ref({
  id: 1,
  title: "这是文章标题",
  content: "这是文章内容",
  author: "这是作者"
});

</script>

<style scoped>

</style>

image-20241117200414006

这样就完成了父组件向子组件的传值。

ref与defineExpose:父调子更随心

有时,父组件需要调用子组件的方法,这就要用 refdefineExpose

场景:父组件需要触发子组件的新增阅读量的操作。

  • 子组件使用defineExpose暴露方法,完整代码:
代码语言:javascript
复制
<template>

  <h1>{{ articleProps.title }}</h1>
  <h4>ID: {{ articleProps.id }}</h4>
  <p>Author: {{ articleProps.author }}</p>
  <p>Read Count: {{readCt }}</p>
  <p>Content: {{ articleProps.content }}</p>

</template>

<script setup>
import {ref} from "vue";

const articleProps = defineProps({
  // 定义id字段,且约定必须传,类型为Number
  id: {
    type: Number,
    required: true
  },
  // 定义title字段,且约定必须传,类型为String,默认值为'default title'
  title: {
    type: String,
    required: true,
    default: 'default title'
  },
  // 定义author字段,且约定必须传,类型为String
  author: {
    type: String,
    required: true
  },
  // 定义content字段,不做任何约束,类型为任意类型
  content: {}
});
const readCt = ref(0);

// 定义暴露给父组件的方法
defineExpose({
  // 定义一个方法,用于增加文章阅读量
  addReadCount() {
    // 这里可以调用一个接口,增加文章阅读量
    readCt.value++;
    alert('阅读量+1')
  }
})
</script>

<style scoped>

</style>
  • 父组件使用ref引用子组件,从而调用子组件的方法:
代码语言:javascript
复制
<template>
  <h1>这是父组件</h1>
  <!-- 引入子组件 BaseArticle 并传入相关属性 -->
  <BaseArticle ref="sonRef" :id="article.id" :title="article.title" :content="article.content"
               :author="article.author"/>
  <!-- 按钮,点击后调用子组件的方法 -->
  <button @click="callSonMethod">调用子组件的方法</button>
</template>

<script setup>
import BaseArticle from "./components/BaseArticle.vue"; // 导入子组件
import {ref} from "vue"; // 从 Vue 中导入 ref

// 创建一个响应式的 article 对象,包含文章的 id、标题、内容和作者
const article = ref({
  id: 1,
  title: "这是文章标题",
  content: "这是文章内容",
  author: "这是作者"
});

// 创建一个 ref 变量,用于引用子组件
const sonRef = ref(null);

// 定义调用子组件方法的函数
const callSonMethod = () => {
  // 调用子组件的 addReadCount 方法
  sonRef.value.addReadCount();
}

</script>

<style scoped>
/* 样式作用范围仅限于此组件 */
</style>

总结:父组件“指挥”,子组件“执行”,母慈子孝。

image-20241117201508643

emits与defineEmits:子传父的桥梁

子组件想给父组件发送消息?在子组中使用defineEmits 或者$emit即可 。

比如说文章阅读的登录限制,登录功能是在子组件中实现的,但是当用户登录成功后,需要及时通知给父组件就可以使用该功能。

  • 在子组件SonEmitsDemo.vue中申明事件
代码语言:javascript
复制
<template>

  <!--  子组件 使用 `$emit` 方法触发自定义事件-->
  <button @click="$emit('sonEmitEvent','这是子组件$emit传递的参数')">这是子组件的$emit按钮</button>
  <br>
  <br>
  <button @click="buttonClick('这是子组件defineEmits传递的参数')">这是子组件的defineEmits按钮</button>


</template>

<script setup>
// 子组件可以显式地通过 defineEmits() 宏来声明它要触发的事件
const emit = defineEmits(['sonDefineEmitsEvent', 'otherEvent'])

function buttonClick(msg) {
  emit('sonDefineEmitsEvent',msg)
}
</script>

<style scoped>

</style>
  • 父组件中通过v-on+事件名,监听:
代码语言:javascript
复制
<template>
  <!--  父组件通过`v-on`监听子组件的事件,不管子组件是通过`$emit`还是`defineEmits`触发的事件,父组件都可以监听到.-->
  <SonEmitsDemo @sonEmitEvent="handleSonEmitsEvent"
                @sonDefineEmitsEvent="handleSonDefineEmitsEvent"
  />
</template>

<script setup>
import SonEmitsDemo from "./components/SonEmitsDemo.vue";

const handleSonEmitsEvent = (msg) => {
  alert(`Received event from son: ${msg}`)
}
const handleSonDefineEmitsEvent = (msg) => {
  alert(`Received event from son: ${msg}`)
}
</script>


<style scoped>

</style>

关键点:父子间的沟通,靠的是一喊一听。效果:

image-20241117204059357

provide与inject:祖组件向后代通信

一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。从上往下传递:

  • provide :父组件提供参数;
  • inject:其他后代组件获取参数;

场景:祖父组件需要给孙子组件传数据。

代码语言:javascript
复制
<template>
  <InjectDemo/>
</template>

<script setup>
import InjectDemo from "./components/InjectDemo.vue";
import {provide} from 'vue'
provide('msgKey', '这是提供的 top组 件的 provide demo 数据')
</script>


<style scoped>

</style>
  • 子组件
代码语言:javascript
复制
<template>
  <h1>这是InjectDemo组件</h1>
  <h2>InjectDemo 监听消息:{{ message }}</h2>

  <Level2InjectDemo/>
</template>


<script setup>

import {inject} from "vue";
import Level2InjectDemo from "./Level2InjectDemo.vue";

const message = inject('msgKey');
</script>


<style scoped>

</style>
  • 孙组件
代码语言:javascript
复制
<template>
  <h1>这是 Level2InjectDemo.vue 组件</h1>
  <h2> Level2InjectDemo.vue 监听消息:{{ message }}</h2>
  <Level3InjectDemo/>
</template>


<script setup>

import {inject} from "vue";
import Level3InjectDemo from "./Level3InjectDemo.vue";

const message = inject('msgKey');

</script>


<style scoped>

</style>
  • 曾孙子组件
代码语言:javascript
复制
<template>
  <h1>这是 Level3InjectDemo.vue 组件</h1>
  <h2> Level3InjectDemo.vue  监听消息:{{ message }}</h2>
</template>


<script setup>

import {inject} from "vue";

const message = inject('msgKey');
</script>


<style scoped>

</style>

解析:就像家族里的老祖宗,遥控指挥第三代的生活。

image-20241117210522970

第三方库 mitt:任意组件通信

provideinject 不够用时,mitt 是一种轻量级的事件总线。一个全局通知系统,任何组件都能触发和接收事件。

使用示例:

  • 先定义Mitt对象实例 MittInstance.js,确保不同vue组件中使用的是同一个事件实例:
代码语言:javascript
复制
import mitt from 'mitt'
export const emitter = mitt()

  • 在任意组件中发送消息:
代码语言:javascript
复制
<template>
  <h2>Producer组件:</h2>
  <button @click="emitEvent">发送emitter事件</button>
</template>

<script setup>
import {emitter} from "./MittInstance.js";


const emitEvent = () => {
  // 发送事件
  emitter.emit('yourCustomEventType', {message: '你好,我是 Producer 组件'})
  console.log('事件已发送')
}

</script>

<style scoped>

</style>
  • 在任意组件中接受消息
代码语言:javascript
复制
<template>
  <h2>Consumer组件,接受到的消息:{{ msg }}</h2>
</template>

<script setup>
import {ref} from "vue";
import {emitter} from "./MittInstance.js";

const msg = ref("还没有收到消息哟")
// 在组件中监听事件
emitter.on('yourCustomEventType', (data) => {
  console.log('Consumer组件收到消息:', data.message)
  msg.value = data.message
})

</script>

<style scoped>

</style>
  • ps:如果没有集成路由,先将生成者和消费者的组件都集成到App.vue方便看效果:
代码语言:javascript
复制
<template>
  <Producer/>
  <Consumer/>
</template>

<script setup>
import Consumer from "./components/mitt/Consumer.vue";
import Producer from "./components/mitt/Producer.vue";
</script>

image-20241117212850397

小贴士mitt 像个广播站,消息传遍每个角落。

ps:但是方才兄并不建议使用mitt,一个项目中,事件使用太多,基本上是没法维护的,更建议使用pinia+watch机制去实现相关的功能,或者从代码实现方案上去避免。

写在最后

截止本篇,vue3系列的内容就输出完毕了,因为方才兄学习前端也仅是为了自己练手,并不考虑面试工作,整个知识体系是基于日常使用的,当然我相信对于初学者,应该是可以帮助到大家去建立一个知识体系的。

如果大家需要去面试前端相关工作,对于vue中重点实现原理是需要去理解和掌握的,这里推荐大家阅读 霍春阳的《Vue.js设计与实现》。

最后附上完整的知识图谱,大家若有需要,公号后台回复【vue】,即可下载xmind原文件。

前端框架-vue系列

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 方才编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是组件
  • 组件关系
  • 如何定义和使用组件
  • 插槽 slot:灵活扩展的秘诀
    • 默认插槽
    • 插槽内容赋值
    • 具名插槽
    • 作用域插槽
  • defineProps:父传子通信神器
  • ref与defineExpose:父调子更随心
  • emits与defineEmits:子传父的桥梁
  • provide与inject:祖组件向后代通信
  • 第三方库 mitt:任意组件通信
  • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档