【Vue3 基础】05.组件化(vue中组件的概念)
ztj100 2025-06-12 19:07 15 浏览 0 评论
这是 Vue3 + Vite + Pinia +TS + Element-Plus 实战系列文档。最近比较忙没什么时间写文章,争取早日把这个系列完结吧~
生命周期和模板引用
在本章之前,我们通过响应式 api 和声明式渲染,处理了 DOM 的更新,但光是这些,对于一些复杂的需要手动操作 DOM 的情况,之前介绍的就无法满足了。
生命周期
每个 Vue 组件在创建时经历的一系列初始化步骤的阶段,我们需要在这些阶段做额外操作的话,需要调用对应阶段的钩子。
这些阶段包括:设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM 等。Vue 官方给出了图示,可以帮助我们更好的理解生命周期:
- setup:红色的所有生命周期 API 都在组件的 setup() 阶段被同步调用。
- 红色方框:不同阶段代表的生命周期,后续我们写的 生命周期钩子 会在此阶段执行。
- 主轴上的:代表组件从初始化到卸载的主要事件。
这里我们先简单介绍,在介绍完生命周期钩子之后,相信你会更理解这张图。
生命周期钩子
了解了上述的生命周期,我们想在对应的周期做一些事情的话,在 Vue3 中我们使用 onXxx 的生命周期钩子,例如:
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
onMounted(() => {
})
onBeforeUnmount(()=>{
})
</script>
如果你使用过 Vue2 的话,你会发现差别:
<script>
export default {
created() {
},
mounted() {
},
beforeDestroy() {
}
}
</script>
使用方法就如前面写的,在 setup 中,在生命周期 API 中注入回调就可以了。这里我们就不去做 Vue2 和 Vue3 的对比了,全当新学的,按照生命周期的顺序:
- setup:beforeCreate 和 created 被 setup 方法代替。
- onBeforeMount():在组件被挂载之前执行回调。组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。
- onMounted():在组件挂载完成后执行回调。通常用于执行需要访问组件所渲染的 DOM 树相关的副作用。
- onBeforeUpdate():在组件即将因为响应式状态变更而更新其 DOM 树之前执行回调。通常用来在 Vue 更新 DOM 之前访问 DOM 状态。
- onUpdated():在组件因为响应式状态变更而更新其 DOM 树之后执行回调。会在组件的任意 DOM 更新后被调用,一般用来访问更新后的 DOM,不能在此做更新 DOM 的操作,可能导致循环。
- onBeforeUnmount():在组件实例被卸载之前执行回调。
- onUnmounted():在组件实例被卸载后执行回调。通常用于手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
- onActivated():当作为 keep-alive 组件被激活时执行回调。
- onDeactivated():当作为 keep-alive 组件被取消激活时执行回调。
- onErrorCaptured():在捕获了后代组件传递的错误时执行回调。通常用来更改组件状态来为用户显示一个错误状态。
- onRenderTracked() 仅开发模式使用 :当组件渲染过程中追踪到响应式依赖时执行回调。通常用于追踪依赖的调试。
- onRenderTriggered() 仅开发模式使用 :当响应式依赖的变更触发了组件渲染时执行回调。通常用于触发更新的调试。
以上就是所有的生命周期以及其可被调用的生命周期钩子,我们在上述钩子中传递回调,Vue 会在其所在的生命周期触发。
模板引用 ref
ref 用于注册元素或者子组件的引用。
模板引用将存储在与名字匹配的 ref 中,例如想在数据加载完之后,更改文字信息或描述信息:
<script setup>
import { ref, onMounted } from "vue";
const h2 = ref(null);
const img = ref(null);
onMounted(() => {
setTimeout(() => {
h2.value.textContent = "数据加载完成";
img.value.src = "/src/assets/logo.svg";
}, 3000);
});
</script>
<template>
<h2 ref="h2">数据加载中...</h2>
<img ref="img" src="./assets/load.svg" alt="" />
</template>
当然我们也完全可以让上述的代码中 h2 变成响应式的,并在h2中使用 {{}} 模板语法实现。
组件引用ref
ref 也可以使用在子组件上,相对来说也是较为常见的用法。
首先解释什么是组件:
在此之前我们都是使用的一个单文件App.vue,如果一个项目将代码全写在这一个文件,那将非常难维护,于是我们把可复用等的页面组件化,页面和逻辑抽离,通过导入组件被其它页面引用。
<script setup>
import Child from './Child.vue'
</script>
<template>
<Child ref="child" />
</template>
使用 ref,父组件能获取子组件示例:
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
})
</script>
<template>
<Child ref="child" />
</template>
需要注意的是,子组件没有使用 <script setup> ,被引用的组件实例和该子组件的 this 完全一致,父组件拥有对子组件的每个属性和方法的访问权。
如果使用 <script setup> 那么子组件默认私有,除非使用 defineExpose 显式暴露。
当然在大部分情况下,使用 props 和 emit 就能实现父子组件的交互,而无需使用 ref。
组件传值 props
组件之间传值的方式主要可以概括为这三类:父子组件传值、兄弟组件传值和远亲组件传值。
Vue 提供给我们组件传值的api有两种:props、emit。其中我们可以通过 props 进行父=>子组件传值。
在开发过程中,我们需要通过 defineProps() 明确子组件的 props。父组件可以像声明 HTML 参数一样传值,也可以使用 : (v-bind 简写) 动态传值:
<!-- 父组件 -->
<script setup>
import { ref } from "vue";
import Children from "./components/Children.vue";
const hello = ref("Hello");
</script>
<template>
<input v-model="hello" />
<Children msg="hhh" :activeMsg="hello" />
</template>
<!-- 子组件 -->
<script lang="ts" setup>
import { onMounted } from "vue";
const props = defineProps({
msg: String,
activeMsg: String
});
onMounted(() => {
console.log("props", props);
});
</script>
<template>
<span>msg: {{ msg }}</span>
<span>activeMsg: {{ activeMsg }}</span>
</template>
defineProps() 声明之后,其中的数据就可以在子组件模板中使用。在 JavaScript 中访问则需要通过 defineProps() 返回的对象访问。
注意:
- props 是只读的,遵循单项数据流,当尝试修改 props 会警告 prop 只读。
- js 中定义数据与 props 中重名时,使用的是 js 中定义的。
const activeMsg = "hl";
<span>activeMsg: {{ activeMsg }}</span>
- props 提供校验选项,保证项目没有使用 TypeScript 进行类型检测,也可以确定一定的数据类型,避免不不满足类型要求的数据传入。(现在 Vue3 支持 TypeScript 可以说这个用处不大了)
interface DataProps {
msg: String;
activeMsg: String;
}
const props = defineProps<DataProps>();
组件监听事件 emit
子组件使用 emit() 向父组件传递数据。第一个参数是事件名称,其它额外参数都会被直接传向父组件的监听器函数。
父组件使用 @ (v-on)监听子组件时间,并且可以接收子组件传递的参数。
<!-- 父组件 -->
<template>
<Children @response="(msg) => (hello = msg)" />
{{ hello }}// 点击子组件按钮后变为 hello from child
</template>
<script setup>
import { ref } from "vue";
import Children from "./components/Children.vue";
const hello = ref("Hello");
</script>
<!-- 子组件 -->
<template>
<button @click="emit('response', 'hello from child')">emit</button>
</template>
<script lang="ts" setup>
const emit = defineEmits(["response"]);
</script>
注意:
- 在模板中可以使用 $emit 的语法,js中只能使用 defineEmits 返回对象 emit 触发事件。
- 可以使用类型标注触发事件,对触发的事件有更精准的控制。
const emit = defineEmits<{
(e: 'response', msg: string): void
}>()
传递模板-插槽 slot
除了传递数据外,父组件还可以通过插槽 slot 的方式将模板传递给子组件。
<!-- 父组件 -->
<template>
<Children>slot button content</Children>
</template>
<script setup>
import Children from "./components/Children.vue";
</script>
<!-- 子组件 -->
<template>
<button><slot /></button>
</template>
<script lang="ts" setup></script>
默认内容
如果想设置默认内容的话,比如在父组件不向子组件传递模板字符,而使用子组件按钮内容有默认 content:
<!-- 父组件 -->
<template>
<Children></Children>
</template>
<script setup>
import Children from "./components/Children.vue";
</script>
<!-- 子组件 -->
<template>
<button>
<slot>
content
</slot>
</button>
</template>
<script lang="ts" setup></script>
具名插槽
如果组件包含多个插槽出口,则需要使用具名插槽,用来给插槽一个唯一 ID,以确定不同出口要渲染的内容。
<!-- 父组件 -->
<template>
<Children>
<template v-slot:header> Header </template>
<template v-slot:button> slot button content </template>
</Children>
</template>
<script setup>
import Children from "./components/Children.vue";
</script>
<template>
<div><slot name="header" /></div>
<button><slot name="button" /></button>
</template>
<script lang="ts" setup></script>
v-slot 可以简写为 # ,v-slot:header => #header。并且v-slot 也可以接受动态参数(动态插槽名): #[dynamicSlotName] 。
作用域插槽
上述的几种插槽,无法访问到子组件的状态,在某些场景中我们想要子组件传递数据给插槽,作用域插槽就可以满足这个需求。
<!-- 父组件 -->
<template>
<Children>
<template v-slot:header="slotProps"> {{ slotProps.msg }}</template>
</Children>
</template>
<script setup>
import Children from "./components/Children.vue";
</script>
<!-- 子组件 -->
<template>
<input type="text" v-model="msg" />
<div><slot name="header" :msg="msg" /></div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const msg = ref("");
</script>
准确来说,上述例子是具名作用域插槽。如果是普通的作用域插槽,即改变 template 的具名插槽为普通插槽就可以了。
实战
前几节讲到了组件的各生命周期钩子、组件传值的 props、触发事件的 emit 以及模板插槽 slot,用的例子比较简单,我们通过实战体验一下在实战开发中的运用,帮助我们更好的理解和运用。
博客的列表展示功能,提供分类功能。
- 分类的数据来源于父组件,其选择的类型通过回调告知父组件。
- 列表部分父组件做元素内容和样式的控制,子组件做列表的基础循环等的通用操作。
示例尽可能的包含到我们这章所学知识,如下述代码部分看不懂,代表你那部分知识还没理解清楚。
本节中的例子里包含部分ts的类型确认
父组件 App.vue:
<script lang="ts" setup>
import { reactive, ref, onMounted } from "vue";
import Children from "./components/Children.vue";
import ClassifyHeader from "./components/ClassifyHeader.vue";
const tags = reactive({
list: ["vue", "react"],
checked: [],
});
function checkedTags(checked) {
tags.checked = checked;
getData();
}
onMounted(() => {
getData();
});
const listRef = ref();
function getData() {
const params = { tags: tags.checked, page: 1 };
console.log("请求参数:", params);
setTimeout(() => {
const data = [
{ title: "JavaScript 入门到精通", username: "Chocolate 1999", date: "2023-02-11" },
{ title: "Vue3 实战", username: "HearLing", date: "2023-03-09" },
];
listRef.value.loadData(data);
}, 1000);
}
</script>
<template>
<ClassifyHeader :tags="tags.list" @select="checkedTags" />
<Children ref="listRef">
<template #item="{ title, username, date }">
<div class="item">
<p>{{ title }}</p>
<p class="meta">作者:{{ username }} | 时间:{{ date }}</p>
</div>
</template>
</Children>
</template>
<style scoped></style>
父组件涉及知识点:
- 与 ClassifyHeader 分类组件的传值。props 和 emit。
- 生命周期 onMounted。在组件挂载后请求数据。
- 组件引用 ref 。使用子组件的方法。
- 具名作用域插槽。
父组件和分类组件
在父组件中,初始的 tags.list 值通过 props 传递给子组件,同时监听了 select 事件。select 触发会执行 checkedTags 函数,父组件得到 checked 值,并做出重新请求列表数据的操作。
components/ClassifyHeader.vue 分类组件:
<!-- 分类 -->
<template>
<div v-for="item in props.tags">
<input type="checkbox" :value="item" @click="select(item)" />
{{ item }}
</div>
</template>
<script lang="ts" setup>
const props = defineProps(["tags"]);
const emit = defineEmits<{
(e: "select", checked: string[]): void;
}>();
const checked: string[] = [];
function select(item) {
const index = checked.indexOf(item);
if (index !== -1) {
checked.splice(index, 1);
} else {
checked.push(item);
}
emit("select", checked);
}
</script>
<style lang="scss" scoped></style>
ClassifyHeader 分类组件主要就是记录所选类别,并通过 emit 传递数据给父组件。
父组件与列表组件
父组件通过 ref 获取到子组件示例,并且使用子组件暴露的 loadData 方法加载数据。子组件通过作用域插槽,传递数据给父组件,父组件控制内容布局。
components/Children.vue 列表组件:
<!-- 子组件 -->
<script lang="ts" setup>
import { ref } from "vue";
interface Item {
title: string;
username: string;
date: string;
}
const items = ref<Item[]>([]);
const loadData = (data) => {
items.value = data;
};
defineExpose({
loadData,
});
</script>
<template>
<ul>
<li v-if="!items.length">Loading...</li>
<li v-for="item in items">
<slot name="item" v-bind="item" />
</li>
</ul>
</template>
<style scoped></style>
子组件,提供具名插槽 slot 以及用 defineExpose 抛出 loadData 方法供父组件通过组件示例拿到。
上述例子,我们就把大部分的知识都重新实战复习了一遍,建议可以把这几个代码自己写一遍,加深印象。
其它传值方式
除了上述的 props、emit 父子组件传值之外。还可以使用依赖注入 API :provide、inject。
- provide():提供一个值,可以被后代组件注入。使用方式:provide(/* 注入名 / 'message', / 值 */ 'hello!') 。
- inject():注入上层组件提供的数据。使用方式:const message = inject('message') 。
兄弟组件,可以可以通过父组件控制数据传值。
对于跨组件通信,我们可以使用状态管理工具,比如我们后面要学的 Pinia,就是一个状态管理框架。
总结
本章中,我们首先结合 Vue 的生命周期的流程图,例举了各个生命周期钩子的触发时机,以及部分钩子的使用场景。然后讲到了 ref 的作用,最后讲完并实战了组件通信相关的 api。
至此 Vue3 的基础知识到这里已经结束了,还剩下一小部分,留在我们实战课程中探索。
如果未学习过TypeScript的话,我也准备了 TypeScript 相关的入门基础知识,课程不会很长,带大家了解 TypeScript 常用的一些知识,为实战做准备。
相关推荐
- Linux集群自动化监控系统Zabbix集群搭建到实战
-
自动化监控系统...
- systemd是什么如何使用_systemd/system
-
systemd是什么如何使用简介Systemd是一个在现代Linux发行版中广泛使用的系统和服务管理器。它负责启动系统并管理系统中运行的服务和进程。使用管理服务systemd可以用来启动、停止、...
- Linux服务器日常巡检脚本分享_linux服务器监控脚本
-
Linux系统日常巡检脚本,巡检内容包含了,磁盘,...
- 7,MySQL管理员用户管理_mysql 管理员用户
-
一、首次设置密码1.初始化时设置(推荐)mysqld--initialize--user=mysql--datadir=/data/3306/data--basedir=/usr/local...
- Python数据库编程教程:第 1 章 数据库基础与 Python 连接入门
-
1.1数据库的核心概念在开始Python数据库编程之前,我们需要先理解几个核心概念。数据库(Database)是按照数据结构来组织、存储和管理数据的仓库,它就像一个电子化的文件柜,能让我们高效...
- Linux自定义开机自启动服务脚本_linux添加开机自启动脚本
-
设置WGCloud开机自动启动服务init.d目录下新建脚本在/etc/rc.d/init.d新建启动脚本wgcloudstart.sh,内容如下...
- linux系统启动流程和服务管理,带你进去系统的世界
-
Linux启动流程Rhel6启动过程:开机自检bios-->MBR引导-->GRUB菜单-->加载内核-->init进程初始化Rhel7启动过程:开机自检BIOS-->M...
- CentOS7系统如何修改主机名_centos更改主机名称
-
请关注本头条号,每天坚持更新原创干货技术文章。如需学习视频,请在微信搜索公众号“智传网优”直接开始自助视频学习1.前言本文将讲解CentOS7系统如何修改主机名。...
- 前端工程师需要熟悉的Linux服务器(SSH 终端操作)指令
-
在Linux服务器管理中,SSH(SecureShell)是远程操作的核心工具。以下是SSH终端操作的常用命令和技巧,涵盖连接、文件操作、系统管理等场景:一、SSH连接服务器1.基本连接...
- Linux开机自启服务完全指南:3步搞定系统服务管理器配置
-
为什么需要配置开机自启?想象一下:电商服务器重启后,MySQL和Nginx没自动启动,整个网站瘫痪!这就是为什么开机自启是Linux运维的必备技能。自启服务能确保核心程序在系统启动时自动运行,避免人工...
- Kubernetes 高可用(HA)集群部署指南
-
Kubernetes高可用(HA)集群部署指南本指南涵盖从概念理解、架构选择,到kubeadm高可用部署、生产优化、监控备份和运维的全流程,适用于希望搭建稳定、生产级Kubernetes集群...
- Linux项目开发,你必须了解Systemd服务!
-
1.Systemd简介...
- Linux系统systemd服务管理工具使用技巧
-
简介:在Linux系统里,systemd就像是所有进程的“源头”,它可是系统中PID值为1的进程哟。systemd其实是一堆工具的组合,它的作用可不止是启动操作系统这么简单,像后台服务...
- Linux下NetworkManager和network的和平共处
-
简介我们在使用CentoOS系统时偶尔会遇到配置都正确但network启动不了的问题,这问题经常是由NetworkManager引起的,关闭NetworkManage并取消开机启动network就能正...
你 发表评论:
欢迎- 一周热门
-
-
MySQL中这14个小玩意,让人眼前一亮!
-
旗舰机新标杆 OPPO Find X2系列正式发布 售价5499元起
-
面试官:使用int类型做加减操作,是线程安全吗
-
C++编程知识:ToString()字符串转换你用正确了吗?
-
【Spring Boot】WebSocket 的 6 种集成方式
-
PyTorch 深度学习实战(26):多目标强化学习Multi-Objective RL
-
pytorch中的 scatter_()函数使用和详解
-
与 Java 17 相比,Java 21 究竟有多快?
-
基于TensorRT_LLM的大模型推理加速与OpenAI兼容服务优化
-
这一次,彻底搞懂Java并发包中的Atomic原子类
-
- 最近发表
-
- Linux集群自动化监控系统Zabbix集群搭建到实战
- systemd是什么如何使用_systemd/system
- Linux服务器日常巡检脚本分享_linux服务器监控脚本
- 7,MySQL管理员用户管理_mysql 管理员用户
- Python数据库编程教程:第 1 章 数据库基础与 Python 连接入门
- Linux自定义开机自启动服务脚本_linux添加开机自启动脚本
- linux系统启动流程和服务管理,带你进去系统的世界
- CentOS7系统如何修改主机名_centos更改主机名称
- 前端工程师需要熟悉的Linux服务器(SSH 终端操作)指令
- Linux开机自启服务完全指南:3步搞定系统服务管理器配置
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)