本文转载自专栏《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:<template>
<h3>这是孙组件</h3>
</template>
<script setup>
</script>
<style scoped>
</style>
SonDemo.vue(子组件2 Son2Demo的代码实现类似):SonDemo.vue使用import关键字导入了孙组件,并在模板<template>中使用了孙组件。<template>
<h2>这是子组件</h2>
<grand-son-demo/>
</template>
<script setup>
import GrandSonDemo from "./GrandSonDemo.vue";
</script>
<style scoped>
</style>
App.vue作为父组件:App.vue使用import关键字导入了子组件,并在模板<template>中使用了子组件。<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
这就完成了组件的定义和使用。组件之前没有内容的联动和通信,接下来,我们就开始父子组件的联动和通信。
插槽是组件间内容分发的“百宝箱”。你可以把插槽理解为子组件给父组件留的一块空地,父组件可以随时填充内容。
注意填充的内容:可以是纯文本,也可以是html代码块(也成为模板内容)。
接下来方才兄结合博客系统的部分实现为例,带大家理解下插槽的使用。
我们在SonDemo.vue的基础上进行修改,增加一个广告栏的插槽:
<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。<slot> 元素内的内容,在外部没有提供任何内容的情况下,就是该插槽的默认内容。<slot> 元素设置一个名字,vue会给一个默认值,等价于<slot name="default">。<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中,传递想要的内容即可:
<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中有两个广告位,一个左侧广告位,一个右侧广告位。
<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>。<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": <div class="slogan_slot">
<slot name="temp_slogan_slot" :tempInt="123">
<p>这是临时插槽,但我可以传递参数给父组件</p>
</slot>
</div>
App.vue中接受参数: <template #temp_slogan_slot="param">
这是被装修后的临时插件,子组件传递的参数为 {{ param.tempInt }}
</template>
效果:

image-20241117194745761
父组件如何把信息“打包”送到子组件?子组件中使用 defineProps接收父组件向子组件传递的属性(props)。
比如说,博客文章详情页的组件BaseArticle.vue,需要由父组件传递文章信息:
<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中传递响应式数据:
<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。
场景:父组件需要触发子组件的新增阅读量的操作。
defineExpose暴露方法,完整代码:<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引用子组件,从而调用子组件的方法:<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
子组件想给父组件发送消息?在子组中使用defineEmits 或者$emit即可 。
比如说文章阅读的登录限制,登录功能是在子组件中实现的,但是当用户登录成功后,需要及时通知给父组件就可以使用该功能。
SonEmitsDemo.vue中申明事件<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+事件名,监听:<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:其他后代组件获取参数;场景:祖父组件需要给孙子组件传数据。
<template>
<InjectDemo/>
</template>
<script setup>
import InjectDemo from "./components/InjectDemo.vue";
import {provide} from 'vue'
provide('msgKey', '这是提供的 top组 件的 provide demo 数据')
</script>
<style scoped>
</style>
<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>
<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>
<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
当 provide 和 inject 不够用时,mitt 是一种轻量级的事件总线。一个全局通知系统,任何组件都能触发和接收事件。
使用示例:
Mitt对象实例 MittInstance.js,确保不同vue组件中使用的是同一个事件实例:import mitt from 'mitt'
export const emitter = mitt()
<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>
<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>
App.vue方便看效果:<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系列