Vue3组件该怎么学?从基础到实战的通关思路有哪些?
想学Vue3组件却不知道从哪发力?组件作为Vue框架的核心复用单元,Vue3在语法、性能、开发体验上都有不少更新,不少开发者刚上手时会纠结“组合式API怎么和组件结合?”“新特性怎么用在实际封装里?”“大型项目组件拆分有啥规律?” 这篇文章把Vue3组件学习拆成「基础认知→核心能力→实战技巧→生态联动」四个模块,用白话问答帮你把知识点串成能用的思路。
先搞懂Vue3组件和Vue2的核心区别?
Vue3组件和Vue2组件最大的区别,得从「代码组织逻辑」和「底层性能」两个维度看。
在Vue2里,我们用选项式API(data
、methods
、computed
这些选项)写组件,代码按“功能分类”——数据放data
,方法放methods
,计算属性放computed
,这种写法适合小项目,但组件逻辑复杂后,比如一个组件要处理表单验证、接口请求、定时任务,代码会分散在不同选项里,找个方法得在data
和methods
之间来回跳,维护起来像“拆盲盒”。
Vue3的组合式API(setup
函数、ref
、reactive
这些)是按“逻辑关注点”组织代码——把相关逻辑(获取用户信息+处理用户权限”)集中写成一个函数,甚至抽成独立的composables
文件,举个例子,以前Vue2里处理用户信息要写:
// Vue2 选项式API export default { data() { return { user: null } }, methods: { fetchUser() { /* 请求逻辑 */ } }, mounted() { this.fetchUser() } }
Vue3组合式API可以这么写:
// Vue3 组合式API <script setup> const user = ref(null) const fetchUser = async () => { user.value = await api.getUser() } onMounted(fetchUser) </script>
你看,请求用户信息的逻辑全堆在一起,不用在不同选项里跳转,这种写法对复杂组件太友好了,逻辑拆分和复用都更顺手。
底层性能上,Vue3的组件渲染基于Proxy做响应式(Vue2用Object.defineProperty
),Proxy能直接监听对象、数组的变化,不用像Vue2那样递归遍历+重写数组方法,大数据列表场景下性能提升明显,而且Vue3新增了像Teleport
(把组件渲染到任意DOM节点,比如弹窗挂到body
)、Suspense
(异步组件加载时的占位逻辑)这些特性,组件的灵活性和工程化能力直接起飞。
那要不要彻底放弃选项式API?没必要!Vue3完全兼容选项式写法,如果你维护老项目,或者小项目想快速开发,选项式照样能用;但新项目建议优先学组合式,因为它更适合复杂逻辑拆分,也是Vue生态未来的方向。
Vue3组件核心知识:Props、Emits、插槽、状态管理怎么玩?
写组件绕不开传值、通信、内容分发这些事儿,Vue3把这些能力做了升级,咱们逐个拆解。
Props:传值更严谨,控制更灵活
Vue3里Props的「严谨性」和「灵活性」都变强了。
先看类型约束:以前Vue2里Props类型写String、Number
这些字符串,Vue3支持直接写构造函数(比如String、Number
,甚至自定义类),还能做更细的验证——用对象形式配置required
、default
、validator
,举个例子:
const props = defineProps({ { type: String, // 类型是字符串 required: true, // 必须传 validator: (val) => val.length > 3 // 自定义验证:长度必须>3 }, pageSize: { type: Number, default: 10 // 默认值 } })
这样传参出错时,控制台会直接报错,比Vue2的提示更直观,团队协作时减少传参歧义。
再看Props透传(inheritAttrs
):如果父组件给子组件传了没在defineProps
里声明的属性,默认会加到子组件根元素上,但有时候我们想自己控制(比如组件根元素是个label
,不想让class、style
自动继承,而是加到内部的input
上),就可以这么玩:
<script setup> import { useAttrs } from 'vue' defineOptions({ inheritAttrs: false }) // 关闭自动继承 const attrs = useAttrs() // 拿到父组件传的未声明属性 </script> <template> <label> <input v-bind="attrs" /> <!-- 手动把属性绑到input上 --> </label> </template>
父组件用<CustomInput class="red-border" data-testid="input" />
时,class
和data-testid
就会精准作用到input
上,不会乱加到根节点label
,这种细粒度控制在封装表单组件时特别实用。
Emits:事件通信更规范
Vue3强制要求用defineEmits
声明自定义事件,好处是「代码即文档」+「参数验证」,比如做一个分页组件<Pagination>
,需要触发'change'
事件传当前页码,就可以:
const emit = defineEmits({ change: (page: number) => { if (typeof page !== 'number' || page < 1) { console.warn('页码必须是大于0的数字') return false // 验证失败,控制台报错 } return true } }) // 调用事件 emit('change', 5) // 合法 emit('change', 0) // 触发验证,控制台报错
父组件用<Pagination @change="handleChange" />
时,再也不用猜“change
事件传什么参数?”,代码可读性和鲁棒性直接拉满。
分发玩出花
分发的关键,Vue3里插槽的玩法更灵活,分三类:
- 默认插槽:子组件用
<slot></slot>
占位,父组件直接写内容; - 具名插槽:子组件用
<slot name="header"></slot>
,父组件用<template #header>
,适合一个组件里插多个区域(比如弹窗的头部、主体、底部); - 作用域插槽:子组件把数据传给父组件的插槽模板,实现「子传父」的反向传值。
举个作用域插槽的复杂例子:写一个<TableWithExpand>
组件,支持点击行展开详情,子组件结构:
<template> <table> <tr v-for="item in list" @click="item.isExpanded = !item.isExpanded"> <td>{{ item.title }}</td> <td> <slot name="expand" :item="item" v-if="item.isExpanded"></slot> </td> </tr> </table> </template> <script setup> defineProps({ list: Array }) </script>
父组件用的时候,自定义展开区域的内容:
<TableWithExpand :list="tableData"> <template #expand="{ item }"> <div class="expand-content"> <p>详情:{{ item.detail }}</p> <Button @click="handleDetail(item.id)">查看更多</Button> </div> </template> </TableWithExpand>
子组件负责管理展开状态,父组件负责渲染展开内容,这种「状态内聚+内容外抛」的设计,让组件既可控又灵活,很多开源UI库的复杂组件都是这么玩的。
组合式API:状态管理=逻辑乐高
组合式API的核心是把重复逻辑抽成composables(可以理解为“逻辑函数”),比如有个获取用户信息+权限判断的逻辑,在多个组件里要用,就写个usePermission.js
:
// usePermission.js import { ref, onMounted } from 'vue' import { useUserStore } from './stores/user' // 假设用Pinia存用户信息 export function usePermission() { const user = useUserStore().user const hasPermission = (key) => { return user.roles.some(role => role.permissions.includes(key)) } // 异步获取最新权限 const fetchPermission = async () => { const res = await api.getPermission() useUserStore().setPermissions(res.data) } onMounted(fetchPermission) return { hasPermission, fetchPermission } }
然后在组件里复用:
<script setup> import { usePermission } from './usePermission' const { hasPermission } = usePermission() </script> <template> <Button v-if="hasPermission('delete')">删除</Button> </template>
把权限判断逻辑从组件里抽走,多个组件复用usePermission
,既减少重复代码,又让组件只关注渲染,维护起来轻松太多,这就是Vue3「逻辑复用」的灵魂——像搭乐高一样组合逻辑。
实战阶段:怎么封装高质量Vue3组件?
封装组件不是堆代码,得先想清楚「谁用这个组件?用它解决啥问题?」,咱们从UI组件、业务组件、性能优化三个角度拆解。
UI组件封装:以Button为例
UI组件(比如Button、Dialog)的核心是「通用性+扩展性」,以Button为例,要考虑这些点:
- 功能分层:基础样式(大小、颜色、圆角)用Props控制,比如
size(small/medium/large)、type(primary/danger)
;交互逻辑(点击防抖、加载状态)用组合式API处理。 - 扩展性:留插槽给自定义内容(比如按钮里加图标
<Button><Icon /></Button>
);用透传attrs
处理class
和style
,让用户能覆盖样式。 - 可访问性:加
aria-label、role
这些属性,适配屏幕阅读器(比如按钮加aria-label="提交表单"
,盲人用户用屏幕阅读器能听到描述)。 - 样式可控:用CSS变量+Scoped CSS+深度选择器,让用户能自定义主题又不破坏组件结构。
代码示例(加载状态+样式自定义):
<script setup> const props = defineProps({ loading: Boolean, type: { type: String, default: 'primary' }, size: { type: String, default: 'medium' } }) const emit = defineEmits(['click']) const handleClick = () => { if (props.loading) return emit('click') } </script> <template> <button :class="[ 'button', `button--${props.type}`, `button--${props.size}` ]" :disabled="props.loading" @click="handleClick" > <slot></slot> <Spinner v-if="props.loading" /> <!-- 假设Spinner是加载组件 --> </button> </template> <style scoped> .button { --btn-primary-color: #42b983; /* 定义CSS变量,用户可覆盖 */ --btn-danger-color: #ff4d4f; background-color: var(--btn-primary-color); /* 其他基础样式 */ } .button--primary { background-color: var(--btn-primary-color); } .button--danger { background-color: var(--btn-danger-color); } /* 深度选择器,让用户能覆盖样式 */ :deep(.custom-class) { border-radius: 0; } </style>
用户用的时候,既能传loading
控制状态,又能通过CSS变量换主题,还能加custom-class
覆盖样式,组件的扩展性拉满。
业务组件封装:以订单列表为例
业务组件(比如订单列表、表单)更考验「场景抽象」能力,以电商的「订单列表」组件为例,要处理商品展示、数量修改、删除、筛选等逻辑,拆分思路是:
- 拆分模块:把「单个订单项」拆成
<OrderItem>
子组件,「筛选条件」拆成<OrderFilter>
子组件; - 逻辑内聚:把筛选逻辑、请求参数处理抽成
useOrderFilter.js
composable; - 事件驱动:子组件
<OrderFilter>
触发'filter-change'
事件,父组件拿到新条件后请求数据,再传给<OrderList>
; - 状态管理:用Pinia存储订单数据(如果多个组件共享),或在组件内用
ref
管理。
代码结构示例:
<!-- OrderList.vue 主组件 --> <script setup> import { ref } from 'vue' import OrderItem from './OrderItem.vue' import OrderFilter from './OrderFilter.vue' import { useOrderFilter } from './useOrderFilter' const props = defineProps({ orders: Array }) const { filterConditions, fetchOrders } = useOrderFilter() // 父组件监听筛选变化,重新请求数据 function handleFilterChange(newConditions) { fetchOrders(newConditions).then(res => { // 更新orders逻辑... }) } </script> <template> <OrderFilter @filter-change="handleFilterChange" /> <div class="order-list"> <OrderItem v-for="order in orders" :order="order" :key="order.id" /> </div> </template> <!-- OrderFilter.vue 子组件 --> <script setup> const emit = defineEmits(['filter-change']) const handleSubmit = () => { // 收集筛选条件 const conditions = { /* 筛选参数 */ } emit('filter-change', conditions) } </script> <template> <div class="filter-bar"> <!-- 筛选表单 --> <Button @click="handleSubmit">筛选</Button> </div> </template> // useOrderFilter.js 逻辑抽离 export function useOrderFilter() { const filterConditions = ref({}) const fetchOrders = async (conditions) => { return await api.getOrders(conditions) } return { filterConditions, fetchOrders } }
这样拆分后,每个模块各司其职:OrderFilter
只负责UI和触发事件,useOrderFilter
处理逻辑,OrderList
做整合,后期要加「导出订单」功能,只需要在useOrderFilter
里加exportOrders
函数,组件里调用就行,不用动其他模块,维护成本直线下降。
性能优化:让组件飞起来
组件性能差?这些Vue3特性能救场:
defineOptions
+inheritAttrs
:关闭不必要的属性继承(inheritAttrs: false
),减少DOM属性更新开销;shallowRef
/markRaw
:对不依赖响应式的数据,用shallowRef
(只监听第一层变化)、markRaw
(标记为非响应式),减少响应式追踪的性能损耗;<KeepAlive>
:包裹动态组件,配合max
属性限制缓存数量,避免重复渲染;defineSlots
:显式声明插槽,让Vue编译器提前优化渲染(比如<script setup> defineSlots({ default: () => {} }) </script>
);- 计算属性+缓存:用
computed
处理依赖变化的计算逻辑,避免重复计算;对复杂列表,用v-for + :key
确保Diff算法高效。
举个shallowRef
的例子:处理大对象(比如图表配置项),只需要在替换整个对象时触发更新,内部属性变化不需要响应式:
<script setup> import { shallowRef } from 'vue' const chartOptions = shallowRef({ xAxis: { type: 'category' }, series: [] }) // 替换整个对象时触发更新(适合大数据场景) chartOptions.value = { ...chartOptions.value, series: newSeries } // 内部属性变化不触发(减少响应式开销) chartOptions.value.series.push(newData) // 这行不会触发更新 </script>
这些优化技巧,在处理复杂页面、大数据列表时,能让组件渲染速度翻倍。
Vue3组件怎么和生态工具联动?
Vue3组件不是孤立的,和生态工具结合才能发挥最大威力,咱们看几个关键联动场景:
和Vite:开发体验拉满
Vite的按需导入(配合unplugin-auto-import
插件)让组件代码更简洁——不用每次import { ref, reactive } from 'vue'
,Vite自动帮
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。