Vue3 怎么写自定义组件?从基础到实战一次讲透
做前端项目时,经常需要把重复功能封装成组件,Vue3 里咋写自定义组件?从基础结构到通信、插槽、性能优化,再到实战案例,这篇文章把关键知识点拆成问答形式,新手也能跟着一步步搞懂~
自定义组件到底是什么?和普通组件有啥区别?
自定义组件,就是自己动手封装功能、结构、样式的可复用单元,比如项目里常用的“弹窗组件”“下拉选择器”,都是自定义组件。
“普通组件”和“自定义组件”没有严格界限,“自定义”更强调“自己封装”(区别于 Element Plus 这类 UI 库的现成组件),核心是通过组件化思想,把重复逻辑、界面拆出来,让代码更干净,改需求时只动一个组件就行。
举个例子:电商项目里每个商品卡片长得一样,把“商品卡片”封装成自定义组件,页面里只要写 <GoodsCard :item="xxx" /> 就能复用,改样式或逻辑时,只改 GoodsCard 这一个文件,所有页面的商品卡片就同步更新了。
写Vue3自定义组件要分哪几步?
最基础的自定义组件,核心是“写结构、写逻辑、写样式,再注册使用”,以单文件组件(.vue 文件)为例,步骤大概这样:
新建组件文件,搭结构
先建个 .vue 文件(MyButton.vue),里面分三部分:
<template>
<!-- 这里写组件的HTML结构 -->
<button class="my-btn">{{ btnText }}</button>
</template>
<script setup>
// 这里写组件的逻辑(数据、方法、props等)
defineProps(['btnText'])
</script>
<style scoped>
/* 这里写组件的样式,scoped保证样式只作用于当前组件 */
.my-btn { padding: 8px 16px; }
</style>
定义Props/Emits,处理组件逻辑
组件要接收外部数据?用 defineProps;要向父组件传消息?用 defineEmits,比如给按钮加点击事件通知父组件:
<template>
<button @click="handleClick">{{ btnText }}</button>
</template>
<script setup>
const props = defineProps({
btnText: {
type: String,
required: true
}
})
const emit = defineEmits(['btn-click'])
const handleClick = () => {
emit('btn-click', '点击事件触发啦~')
}
</script>
注册组件,在其他页面使用
-
局部注册:在要用到的页面里,导入 + 注册:
<script setup> import MyButton from './components/MyButton.vue' </script> <template> <MyButton btnText="提交" @btn-click="handleClick" /> </template>
-
全局注册:在 main.js 里全局注册,所有页面直接用:
import { createApp } from 'vue' import App from './App.vue' import MyButton from './components/MyButton.vue' const app = createApp(App) app.component('MyButton', MyButton) app.mount('#app')
组件间通信咋处理?Props和Emits怎么用?
组件通信是自定义组件的核心!Vue3 里最基础的“父传子用 Props,子传父用 Emits”,还有 v-model 双向绑定,一个个说:
Props:父组件给子组件传数据
定义 Props 时,要明确类型、是否必填、默认值,避免传参出错,比如做个“用户信息卡片”组件:
<script setup>
const props = defineProps({
user: {
type: Object, // 类型是对象
required: true, // 必须传
default: () => ({ name: '匿名', age: 0 }) // 若没传,用默认值(对象/数组要写函数返回,避免引用问题)
},
theme: {
type: String,
validator: (val) => ['light', 'dark'].includes(val) // 自定义验证规则,只能传light或dark
}
})
</script>
<template>
<div class="card" :class="theme">
<h3>{{ user.name }}</h3>
<p>年龄:{{ user.age }}</p>
</div>
</template>
父组件用的时候:<UserCard :user="currentUser" theme="dark" />
Emits:子组件给父组件发消息
子组件触发事件,父组件监听,比如卡片里的“编辑”按钮,点击后通知父组件:
<template>
<div class="card">
<button @click="handleEdit">编辑</button>
</div>
</template>
<script setup>
const emit = defineEmits(['edit-user']) // 声明要触发的事件
const handleEdit = () => {
emit('edit-user', '当前用户ID') // 传参给父组件
}
</script>
父组件监听:<UserCard @edit-user="openEditModal" />
v-model 双向绑定(Vue3 更简洁)
以前写 .sync 很麻烦,Vue3 里子组件用 defineModel 能直接实现双向绑定(需 Vue3.4+),开关组件”,父组件控制开关状态,子组件也能改:
子组件:
<template>
<button @click="toggle">{{ modelValue ? '开' : '关' }}</button>
</template>
<script setup>
const modelValue = defineModel() // 自动关联v-model的值
const toggle = () => {
modelValue.value = !modelValue.value
}
</script>
父组件用:<Switch v-model="isOpen" />,不用手动写 :value 和 @update:value 了,超方便~
想让组件更灵活?插槽(Slot)怎么玩?
插槽是让“父组件能自定义子组件部分内容”的神器!比如弹窗组件,不同页面的弹窗标题、内容不一样,用插槽就能灵活配置。
默认插槽:最简单的自定义内容
子组件里留个 <slot>,父组件在标签里写内容,会自动填充到 slot 位置,卡片组件”的底部操作区:
子组件 Card.vue:
<template>
<div class="card">
<header>{{ title }}</header>
<main>{{ content }}</main>
<footer>
<slot>默认内容(父组件没传的话显示这个)</slot>
</footer>
</div>
</template>
父组件用:
<Card title="文章" content="这是文章内容"> <button>点赞</button> <button>收藏</button> </Card>
这样 footer 里就会显示两个按钮,没传的话显示“默认内容”。
具名插槽:多个插槽,指定位置
如果组件有多个可自定义区域(比如弹窗的“标题”和“内容”),用 name 区分,子组件 Popup.vue:
<template>
<div class="popup">
<div class="title">
<slot name="title">默认标题</slot>
</div>
<div class="content">
<slot name="content">默认内容</slot>
</div>
</div>
</template>
父组件用 v-slot: 或 指定插槽名:
<Popup>
<template #title>
<h2>重要通知</h2>
</template>
<template #content>
<p>系统即将维护,请提前保存数据~</p>
</template>
</Popup>
作用域插槽:子组件给插槽传数据
子组件里的 slot 可以传数据,父组件用的时候能拿到,列表组件”,每个列表项的样式由父组件定,但数据来自子组件,子组件 List.vue:
<template>
<ul>
<li v-for="item in list" :key="item.id">
<slot :item="item" /> <!-- 把item传给父组件 -->
</li>
</ul>
</template>
<script setup>
defineProps(['list'])
</script>
父组件用的时候,通过 v-slot="scope" 接收数据:
<List :list="products">
<template #default="scope">
<!-- scope.item 就是子组件传的每个商品数据 -->
<div class="product-item">
<img :src="scope.item.img" />
<p>{{ scope.item.name }}</p>
</div>
</template>
</List>
用组合式API封装逻辑,自定义组件咋复用代码?
Vue3 的组合式 API(ref、computed、watch)+ 自定义组合式函数(Composables),能把组件里的重复逻辑抽出来,让自定义组件更简洁。
啥是组合式函数?
就是把一组相关的逻辑(请求数据 + 加载状态 + 错误处理”)封装成一个函数,在多个组件里复用,比如写个 useFetch 处理接口请求:
// useFetch.js
import { ref, onMounted } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(true)
const error = ref(null)
const fetchData = async () => {
try {
const res = await fetch(url)
data.value = await res.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
onMounted(fetchData)
return { data, loading, error }
}
在自定义组件里用组合式函数
比如做个“文章列表组件”,用 useFetch 拿数据:
<template>
<div class="article-list">
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error.message }}</div>
<ul v-else>
<li v-for="item in data" :key="item.id">{{ item.title }}</li>
</ul>
</div>
</template>
<script setup>
import { useFetch } from './useFetch.js'
const { data, loading, error } = useFetch('https://api.example.com/articles')
</script>
这样不管是“文章列表”还是“商品列表”,只要需要请求数据,都能复用 useFetch,组件里只关心 UI 渲染,逻辑全在组合式函数里,代码清爽多了~
自定义组件性能咋优化?这些细节要注意
自定义组件写多了,性能问题容易冒出来,特别是列表、频繁更新的组件,这些小技巧能帮组件跑得更快:
减少不必要的响应式
Vue3 的响应式是基于 Proxy 的,但有些数据不需要“响应式”(比如纯展示的静态数据),用 shallowRef 或 markRaw 跳过响应式追踪:
<script setup>
import { shallowRef } from 'vue'
// 比如一个很大的静态配置对象,不需要响应式
const staticConfig = shallowRef({ /* 大量数据 */ })
</script>
用 v-once
如果组件里有“首次渲染后不会变”的内容,加 v-once 让 Vue 只渲染一次,不再追踪更新:
<template>
<div class="static-info" v-once>
<!-- 这里的内容渲染后永远不变 -->
<p>系统版本:{{ version }}</p>
<p>更新时间:{{ updateTime }}</p>
</div>
</template>
事件处理函数缓存
如果组件里的事件处理函数依赖了 props 或 reactive 数据,每次渲染都会生成新函数,导致子组件不必要更新,用 useCallback 缓存函数(但注意,Vue3 的 script setup 里要结合 defineProps 等使用):
<script setup>
import { useCallback } from 'vue'
const props = defineProps(['item'])
// 缓存handleClick,只有props.item变了才重新生成函数
const handleClick = useCallback(() => {
console.log('点击了', props.item.id)
}, [props.item])
</script>
<template>
<button @click="handleClick">点击</button>
</template>
v-memo 跳过不必要的更新
在列表渲染时,用 v-memo 告诉 Vue:“这些数据没变的话,别更新这部分 DOM”,比如表格的某一列:
<template>
<table>
<tr v-for="item in list" :key="item.id">
<td v-memo="[item.name]">{{ item.name }}</td>
<td>{{ item.price }}</td>
</tr>
</table>
</template>
只有 item.name 变了,才会更新第一列,其他情况跳过,减少 DOM 操作~
实战:做个带搜索的下拉选择组件,巩固知识点
光说不练假把式!现在做个“SearchSelect”组件,需求是:
- 父组件传选项列表(options)
- 输入框搜索,实时过滤选项
- 点击选项,把值传给父组件
- 支持自定义选项的显示内容(用插槽)
分析结构,写基础模板
组件需要:输入框、下拉列表、每个选项,先搭结构:
<template>
<div class="search-select">
<!-- 搜索输入框 -->
<input
v-model="searchVal"
placeholder="搜索..."
class="search-input"
>
<!-- 下拉列表 -->
<div class="options" v-show="showOptions">
<div
v-for="item in filteredOptions"
:key="item.value"
@click="handleSelect(item)"
class="option"
>
<!-- 插槽:让父组件自定义选项显示 -->
<slot :item="item">{{ item.label }}</slot>
</div>
</div>
</div>
</template>
处理逻辑(Props、Emits、响应式数据)
用 defineProps 接收 options,defineEmits 触发选择事件,ref 存搜索值和下拉显示状态:
<script setup>
import { ref, computed } from 'vue'
// 接收父组件的选项列表
const props = defineProps({
options: {
type: Array,
required: true,
default: () => []
}
})
// 触发选择事件
const emit = defineEmits(['select'])
// 响应式数据:搜索值、下拉是否显示
const searchVal = ref('')
const showOptions = ref(false)
// 计算属性:过滤选项(搜索值匹配label)
const filteredOptions = computed(() => {
return props.options.filter(item =>
item.label.toLowerCase().includes(searchVal.value.toLowerCase())
)
})
// 点击选项的处理函数
const handleSelect = (item) => {
emit('select', item.value) // 把值传给父组件
searchVal.value = '' // 清空搜索框
showOptions.value = false // 收起下拉
}
// 输入时显示下拉
watch(searchVal, (newVal) => {
showOptions.value = newVal.length > 0
})
</script>
样式处理(Scoped CSS)
给组件加样式,区分选中态、 hover 态:
<style scoped>
.search-select {
width: 200px;
position: relative;
}
.search-input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.options {
position: absolute;
top: 40px;
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
max-height: 200px;
overflow-y: auto;
}
.option {
padding: 8px;
cursor: pointer;
}
.option:hover {
background: #f5f5f5;
}
</style>
父组件使用,测试功能
在页面里导入 SearchSelect,
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。