Vue 自定义组件完全指南:从零构建待办事项应用
本文通过从零构建一个「待办事项应用」,深入讲解 Vue 自定义组件的核心概念与最佳实践。所有示例代码均为原创设计,帮助你理解组件化开发的精髓。
目录
- 为什么需要自定义组件
- 组件的基本结构
- 组件注册与引入
- Props:父传子的数据通道
- Emits:子传父的事件机制
- 模板语法实战
- 计算属性与侦听器
- 生命周期钩子
- 组件通信模式
- 样式隔离与 Scoped CSS
- 实战:构建待办事项应用
- 最佳实践总结
为什么需要自定义组件
在现代前端开发中,组件化思维是构建大型应用的基础。想象一下,你正在开发一个待办事项应用——界面包含标题栏、添加表单、任务列表、筛选器等多个部分。如果把所有逻辑写在一个文件里,代码会变得臃肿且难以维护。
自定义组件带来的好处:
| 优势 | 说明 |
|---|---|
| 可复用性 | 同一个组件可以在多个地方使用,减少重复代码 |
| 可维护性 | 每个组件职责单一,修改时影响范围可控 |
| 可测试性 | 小组件更容易编写单元测试 |
| 团队协作 | 不同开发者可以并行开发不同组件 |
| 代码可读性 | 组件名称本身就是文档,一目了然 |
以我们的待办事项应用为例,整个应用可以拆分为以下组件树:
App.vue (根组件 - 管理所有状态)
├── AppHeader.vue -- 应用标题栏
├── TodoInput.vue -- 待办输入表单
├── TodoFilter.vue -- 筛选器(全部/进行中/已完成)
└── TodoList.vue -- 待办列表容器
└── TodoItem.vue -- 单个待办项
这种层级结构让代码组织清晰,每个组件只关心自己的逻辑。
组件的基本结构
一个 Vue 单文件组件(SFC)由三个部分组成:
┌─────────────────────────────────────┐
│ <template> │
│ 组件的 HTML 模板 │
│ 定义组件长什么样 │
│ </template> │
├─────────────────────────────────────┤
│ <script setup> │
│ 组件的 JavaScript/TypeScript 逻辑 │
│ 定义组件的行为 │
│ </script> │
├─────────────────────────────────────┤
│ <style scoped> │
│ 组件的 CSS 样式 │
│ scoped 确保样式只作用于当前组件 │
│ </style> │
└─────────────────────────────────────┘
<script setup> 是 Vue 3 引入的组合式 API 语法糖,它让代码更简洁:
- 不需要
export default - 顶层的变量和函数自动暴露给模板
- 不需要
components选项注册组件
组件注册与引入
局部注册(推荐)
在 <script setup> 中,引入即注册:
// 1. 引入组件
import AppHeader from './components/AppHeader.vue'
import TodoInput from './components/TodoInput.vue'
import TodoList from './components/TodoList.vue'
// 2. 直接在模板中使用,无需额外注册
这种方式的好处是:
- 按需加载,未使用的组件不会被打包
- 作用域明确,不会污染全局
- 代码可读性强,一眼看出组件来源
全局注册
如果你希望一个组件在所有地方都能使用,可以在应用入口全局注册:
// main.js 中的应用入口逻辑
import { createApp } from 'vue'
import App from './App.vue'
import BaseButton from './components/BaseButton.vue'
const app = createApp(App)
// 全局注册组件,之后可以在任何地方使用 <BaseButton />
app.component('BaseButton', BaseButton)
app.mount('#app')
何时使用全局注册?
- 极高频使用的基础组件(如按钮、输入框、图标)
- UI 库的组件
何时使用局部注册?
- 大部分业务组件
- 只在特定页面使用的组件
💡 最佳实践:默认使用局部注册,只在确实需要时才全局注册。这有助于保持打包体积最小化。
Props:父传子的数据通道
Props 是父组件向子组件传递数据的主要方式。它遵循单向数据流原则——数据只能从父流向子,不能反向修改。
定义 Props
在子组件中,使用 defineProps 宏声明接收的 props:
// 子组件 TodoItem.vue 中
defineProps({
todo: {
type: Object,
required: true // 必填项
},
index: {
type: Number,
default: 0 // 默认值
}
})
Prop 验证
Vue 提供了强大的 prop 类型验证:
| 选项 | 说明 | 示例 |
|---|---|---|
type |
类型检查 | String, Number, Boolean, Object, Array, Function |
required |
是否必填 | true / false |
default |
默认值 | 基础类型直接写,引用类型用工厂函数 |
validator |
自定义验证 | (value) => ['active', 'done'].includes(value) |
实际应用场景
在我们的待办应用中,Props 的使用非常典型:
TodoList 组件接收待办数组:
// 父组件 App.vue 传递数据
<TodoList
:todos="filteredTodos"
:loading="isLoading"
/>
// 子组件 TodoList.vue 接收数据
defineProps({
todos: {
type: Array,
default: () => [] // 数组/对象类型必须用工厂函数
},
loading: {
type: Boolean,
default: false
}
})
TodoItem 组件接收单个待办:
// 父组件 TodoList.vue 中遍历传递
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
/>
// 子组件 TodoItem.vue
defineProps({
todo: {
type: Object,
required: true,
validator: (value) => {
// 自定义验证:确保 todo 对象包含必要字段
return value.id && value.title !== undefined
}
}
})
⚠️ 注意:以
:开头的 prop 表示传递的是 JavaScript 表达式,而不是字符串。
单向数据流
父组件 子组件
┌──────────┐ Props ┌──────────┐
│ todos │ ──────────> │ todo │
│ │ │ (只读) │
└──────────┘ └──────────┘
↑ │
│ Emits/回调 │
└────────────────────────┘
如果子组件需要修改 prop 的值,应该通过事件通知父组件,而不是直接修改。
Emits:子传父的事件机制
当子组件需要通知父组件某些事情发生时,使用 emits。这是 Vue 组件通信的核心机制之一。
定义 Emits
// 子组件 TodoInput.vue 中
const emit = defineEmits(['add'])
function handleSubmit() {
const text = inputText.value.trim()
if (!text) return
// 触发事件,传递新待办的标题
emit('add', text)
inputText.value = ''
}
父组件监听事件
// 父组件 App.vue 中
<TodoInput @add="handleAddTodo" />
// 处理函数
function handleAddTodo(title) {
todos.value.push({
id: Date.now(),
title: title,
completed: false
})
}
事件命名规范
| 风格 | 示例 | 说明 |
|---|---|---|
| kebab-case | @toggle-complete |
推荐,与 HTML 属性风格一致 |
| camelCase | @toggleComplete |
也可用,但在 DOM 模板中需要转换 |
实际应用:TodoItem 组件的多事件
// 子组件 TodoItem.vue
const emit = defineEmits(['toggle', 'delete', 'edit'])
// 切换完成状态
function handleToggle() {
emit('toggle', props.todo.id)
}
// 删除待办
function handleDelete() {
emit('delete', props.todo.id)
}
// 编辑待办(进入编辑模式)
function handleEdit() {
emit('edit', props.todo.id)
}
// 父组件 TodoList.vue
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@toggle="handleToggle"
@delete="handleDelete"
@edit="handleEdit"
/>
带类型定义的 Emits(推荐)
为了更好地进行类型检查,可以定义 emits 的验证:
const emit = defineEmits({
// 带验证函数
add: (title) => {
if (!title || title.trim() === '') {
console.warn('Invalid add event payload!')
return false
}
return true
},
// 不验证
delete: null
})
模板语法实战
Vue 的模板语法强大而灵活,让我们看看实际项目中的常用技巧。
条件渲染
根据待办状态显示不同的 UI:
<!-- 根据完成状态显示不同图标 -->
<span v-if="todo.completed" class="icon-done">✓</span>
<span v-else class="icon-pending">○</span>
<!-- 空状态提示 -->
<div v-if="todos.length === 0" class="empty-state">
<p>暂无待办事项</p>
<p class="hint">添加一个新的待办吧</p>
</div>
<!-- 加载状态 -->
<div v-else-if="loading" class="loading">加载中...</div>
<!-- 正常列表 -->
<div v-else class="todo-list">
<!-- 列表内容 -->
</div>
列表渲染
遍历待办数组,为每个待办创建组件实例:
<TodoItem
v-for="(todo, index) in todos"
:key="todo.id"
:todo="todo"
:index="index"
@toggle="handleToggle"
@delete="handleDelete"
/>
💡 关键点:
key属性帮助 Vue 追踪每个节点的身份,确保列表更新时高效复用 DOM。永远使用唯一标识(如 id)作为 key,避免使用 index。
动态类绑定
根据状态动态切换 CSS 类:
<!-- 单个类 -->
<div :class="{ completed: todo.completed }">
{{ todo.title }}
</div>
<!-- 多个类 -->
<div :class="{
'todo-item': true,
'is-completed': todo.completed,
'is-overdue': isOverdue(todo.deadline)
}">
<!-- 数组语法 -->
<div :class="[baseClass, todo.completed ? 'completed' : '']">
<!-- 结合普通 class -->
<div class="todo-item" :class="{ completed: todo.completed }">
动态样式绑定
<!-- 对象语法 -->
<div :style="{ color: priorityColor(todo.priority) }">
<!-- 多个样式 -->
<div :style="{
color: priorityColor(todo.priority),
fontWeight: todo.important ? 'bold' : 'normal'
}">
事件处理
<!-- 基础事件 -->
<button @click="handleClick">点击</button>
<!-- 带参数 -->
<button @click="handleDelete(todo.id)">删除</button>
<!-- 事件修饰符 -->
<form @submit.prevent="handleSubmit">...</form>
<input @keydown.enter.exact="handleAdd" />
<!-- 阻止冒泡:点击删除按钮不触发父元素的点击 -->
<button @click.stop="handleDelete">删除</button>
<!-- 只触发一次 -->
<button @click.once="handleFirstClick">只触发一次</button>
双向绑定
<!-- 输入框双向绑定 -->
<input v-model="newTodoTitle" placeholder="添加新待办..." />
<!-- 复选框 -->
<input type="checkbox" v-model="todo.completed" />
<!-- 下拉选择 -->
<select v-model="selectedFilter">
<option value="all">全部</option>
<option value="active">进行中</option>
<option value="completed">已完成</option>
</select>
计算属性与侦听器
计算属性(Computed)
计算属性基于响应式依赖进行缓存,只有依赖变化时才重新计算:
// 根据筛选条件过滤待办
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed)
case 'completed':
return todos.value.filter(t => t.completed)
default:
return todos.value
}
})
// 统计未完成数量
const activeCount = computed(() => {
return todos.value.filter(t => !t.completed).length
})
// 统计已完成数量
const completedCount = computed(() => {
return todos.value.filter(t => t.completed).length
})
// 是否所有待办都已完成
const allCompleted = computed(() => {
return todos.value.length > 0 &&
todos.value.every(t => t.completed)
})
计算属性 vs 方法:
| 特性 | 计算属性 | 方法 |
|---|---|---|
| 缓存 | ✅ 有缓存,依赖不变不重算 | ❌ 每次调用都执行 |
| 响应式 | ✅ 自动追踪依赖 | ❌ 需要手动调用 |
| 适用场景 | 数据转换、过滤、派生状态 | 复杂逻辑、副作用操作 |
| 性能 | 更好 | 相对较差 |
侦听器(Watch)
当需要在数据变化时执行副作用(如本地存储、API 请求)时使用:
// 监听待办变化,自动保存到本地存储
watch(
todos,
(newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos))
},
{ deep: true }
)
// 监听筛选条件变化,记录日志
watch(filter, (newFilter, oldFilter) => {
console.log(`筛选条件从 ${oldFilter} 变为 ${newFilter}`)
})
// 立即执行的侦听器
watch(
todos,
(newTodos) => {
updateStatistics(newTodos)
},
{ immediate: true } // 初始化时立即执行一次
)
侦听器选项:
| 选项 | 说明 |
|---|---|
deep: true |
深度监听对象/数组内部变化 |
immediate: true |
立即执行一次 |
flush: 'post' |
在 DOM 更新后执行 |
侦听器 vs 计算属性
// ❌ 不推荐:用侦听器做计算属性的事
watch(todos, (newTodos) => {
activeCount.value = newTodos.filter(t => !t.completed).length
})
// ✅ 推荐:用计算属性
const activeCount = computed(() => {
return todos.value.filter(t => !t.completed).length
})
💡 经验法则:如果目的是派生数据,用计算属性;如果目的是执行副作用,用侦听器。
生命周期钩子
每个 Vue 组件实例在创建时都会经历一系列初始化步骤。Vue 提供了生命周期钩子,让你在特定阶段执行代码。
常用生命周期钩子
┌─────────────────────────────────────────────────────────────┐
│ 组件生命周期 │
├─────────────────────────────────────────────────────────────┤
│ │
│ beforeCreate ──> created ──> beforeMount ──> mounted │
│ │ │
│ v │
│ beforeUnmount <── unmounted <── beforeUpdate <── updated │
│ │
└─────────────────────────────────────────────────────────────┘
| 钩子 | 时机 | 常见用途 |
|---|---|---|
onMounted |
组件挂载到 DOM 后 | 初始化第三方库、发起 API 请求、操作 DOM |
onUpdated |
数据变化导致 DOM 更新后 | 操作更新后的 DOM |
onUnmounted |
组件从 DOM 卸载后 | 清理定时器、取消订阅、释放资源 |
onBeforeMount |
挂载开始前 | 最后一次修改数据的机会(不会触发更新) |
onBeforeUnmount |
卸载开始前 | 清理工作的最后时机 |
实际应用
// 组件挂载时从本地存储加载待办
onMounted(() => {
const saved = localStorage.getItem('todos')
if (saved) {
todos.value = JSON.parse(saved)
}
})
// 组件卸载时清理定时器
let timer = null
onMounted(() => {
timer = setInterval(() => {
checkReminders()
}, 60000) // 每分钟检查一次
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
生命周期与侦听器的选择
// ❌ 不推荐:在 mounted 中设置侦听
onMounted(() => {
watch(todos, saveToStorage, { deep: true })
})
// ✅ 推荐:直接在 setup 中设置侦听器
watch(todos, saveToStorage, { deep: true })
// ✅ 推荐:mounted 只用于 DOM 操作
onMounted(() => {
inputRef.value?.focus()
})
组件通信模式
1. 父传子:Props
最常用、最推荐的方式:
// 父组件
<TodoItem :todo="todoData" :index="0" />
// 子组件
defineProps({
todo: { type: Object, required: true },
index: { type: Number, default: 0 }
})
2. 子传父:Emits
子组件通知父组件执行操作:
// 子组件
const emit = defineEmits(['delete'])
emit('delete', todoId)
// 父组件
<TodoItem @delete="handleDelete" />
3. 双向绑定:v-model
简化父子组件的双向数据同步:
// 子组件 TodoInput.vue
const modelValue = defineModel() // Vue 3.4+ 语法
// 或手动实现
defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
function updateValue(newValue) {
emit('update:modelValue', newValue)
}
// 父组件
<TodoInput v-model="newTodoText" />
<!-- 等价于 -->
<TodoInput
:modelValue="newTodoText"
@update:modelValue="newTodoText = $event"
/>
4. 祖先传后代:Provide/Inject
当组件层级很深时,避免逐层传递 props:
// 根组件 App.vue
import { provide } from 'vue'
// 提供主题配置,任何后代组件都可以获取
provide('theme', {
primaryColor: '#42b883',
darkMode: false
})
// 深层后代组件 DeepChild.vue
import { inject } from 'vue'
const theme = inject('theme', {
// 默认值(当祖先没有 provide 时使用)
primaryColor: '#000',
darkMode: false
})
5. 模板引用:ref / defineExpose
直接访问子组件实例或 DOM 元素:
<!-- 父组件模板 -->
<TodoInput ref="todoInputRef" />
<!-- 父组件脚本 -->
import { ref, onMounted } from 'vue'
const todoInputRef = ref(null)
onMounted(() => {
// 调用子组件暴露的方法
todoInputRef.value.focus()
})
// 子组件 TodoInput.vue - 使用 defineExpose 暴露方法
const inputRef = ref(null)
function focus() {
inputRef.value?.focus()
}
function clear() {
inputText.value = ''
}
// 只暴露需要的方法给父组件
defineExpose({ focus, clear })
6. 跨组件通信:事件总线
适用于不相关组件之间的通信(谨慎使用):
// event-bus.js
import mitt from 'mitt'
export const emitter = mitt()
// 组件 A:发送事件
import { emitter } from './event-bus'
emitter.emit('todo-added', newTodo)
// 组件 B:监听事件
import { emitter } from './event-bus'
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
emitter.on('todo-added', handleTodoAdded)
})
onUnmounted(() => {
emitter.off('todo-added', handleTodoAdded)
})
⚠️ 注意:事件总线会使数据流难以追踪,优先使用 Props/Emits 或 Provide/Inject。
样式隔离与 Scoped CSS
Scoped 样式
在 <style> 标签上添加 scoped 属性,样式只会作用于当前组件:
<style scoped>
.todo-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.todo-item.completed .title {
text-decoration: line-through;
color: #999;
}
</style>
Vue 会自动为选择器添加数据属性,确保样式隔离:
/* 编译前 */
.todo-item { ... }
/* 编译后 */
.todo-item[data-v-xxxx] { ... }
深度选择器
当需要影响子组件的样式时,使用 :deep() 伪类:
<style scoped>
/* 影响子组件的内部元素 */
:deep(.child-component .inner-element) {
color: red;
}
/* 更具体的示例:自定义第三方组件样式 */
:deep(.el-input__wrapper) {
border-radius: 8px;
}
</style>
插槽选择器
为插槽内容添加样式:
<style scoped>
/* 插槽内容的样式 */
:slotted(.custom-content) {
font-weight: bold;
color: #42b883;
}
</style>
全局样式
如果某些样式需要全局生效,可以使用单独的 <style> 标签(不带 scoped):
<!-- 组件级全局样式 -->
<style>
/* 这些样式会应用到整个应用 */
.text-primary { color: #42b883; }
.text-danger { color: #e74c3c; }
.btn { padding: 8px 16px; border-radius: 4px; }
</style>
<!-- 组件级局部样式 -->
<style scoped>
/* 这些样式只在当前组件生效 -->
.todo-item { background: white; }
</style>
CSS 变量
使用 CSS 变量实现主题切换:
<!-- App.vue -->
<style>
:root {
--primary-color: #42b883;
--bg-color: #ffffff;
--text-color: #333333;
}
[data-theme="dark"] {
--primary-color: #64d6a6;
--bg-color: #1a1a1a;
--text-color: #ffffff;
}
</style>
<style scoped>
.todo-app {
background: var(--bg-color);
color: var(--text-color);
}
.btn-primary {
background: var(--primary-color);
}
</style>
实战:构建待办事项应用
让我们通过一个完整的待办事项应用案例,串联所有知识点。
项目结构
src/
├── components/
│ ├── App.vue -- 根组件:状态管理
│ ├── AppHeader.vue -- 应用标题栏
│ ├── TodoInput.vue -- 待办输入表单
│ ├── TodoFilter.vue -- 筛选器
│ ├── TodoList.vue -- 待办列表
│ └── TodoItem.vue -- 单个待办项
├── composables/
│ └── useTodos.js -- 待办逻辑组合式函数
├── utils/
│ └── storage.js -- 本地存储工具
├── styles/
│ └── main.css -- 全局样式
└── main.js -- 入口文件
组件职责划分
| 组件 | 职责 | 接收的 Props | 触发的 Emits |
|---|---|---|---|
| App | 状态管理、协调各组件 | - | - |
| AppHeader | 显示标题和统计信息 | title, activeCount, completedCount | - |
| TodoInput | 待办输入、添加功能 | placeholder | add |
| TodoFilter | 筛选状态切换 | currentFilter | update:filter |
| TodoList | 待办列表展示 | todos, loading | toggle, delete, edit |
| TodoItem | 单个待办的展示与交互 | todo | toggle, delete, edit |
数据流向
用户操作
│
▼
┌──────────┐ add ┌──────────┐
│TodoInput │ ──────────> │ App │
└──────────┘ │ │
│ 状态管理 │
┌──────────┐ todos │ │
│ TodoList │ <────────── │ │
│ │ │ │
│ ┌──────┐ │ │ │
│ │TodoItem│ │ │ │
│ └──────┘ │ └──────────┘
└──────────┘
│
│ toggle/delete/edit
▼
App 处理
核心实现要点
1. 状态提升
将共享状态放在最近的共同祖先组件中:
// App.vue 管理所有共享状态
const todos = ref([]) // 待办列表
const filter = ref('all') // 当前筛选
const isLoading = ref(false) // 加载状态
// 计算属性:根据筛选过滤
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed)
case 'completed':
return todos.value.filter(t => t.completed)
default:
return todos.value
}
})
2. 添加待办
// App.vue
function handleAddTodo(title) {
todos.value.push({
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
})
}
// TodoInput.vue
const emit = defineEmits(['add'])
const inputText = ref('')
function handleSubmit() {
const text = inputText.value.trim()
if (!text) return
emit('add', text)
inputText.value = ''
}
3. 切换完成状态
// App.vue
function handleToggle(id) {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
// TodoItem.vue
const emit = defineEmits(['toggle'])
function handleToggle() {
emit('toggle', props.todo.id)
}
4. 本地存储持久化
// App.vue
import { watch, onMounted } from 'vue'
// 初始化时加载
onMounted(() => {
const saved = localStorage.getItem('todos')
if (saved) {
todos.value = JSON.parse(saved)
}
})
// 变化时自动保存
watch(todos, (newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })
5. 组合式函数(Composables)
将可复用的逻辑提取到独立函数:
// composables/useTodos.js
import { ref, computed, watch, onMounted } from 'vue'
export function useTodos() {
const todos = ref([])
const filter = ref('all')
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed)
case 'completed':
return todos.value.filter(t => t.completed)
default:
return todos.value
}
})
const activeCount = computed(() =>
todos.value.filter(t => !t.completed).length
)
const completedCount = computed(() =>
todos.value.filter(t => t.completed).length
)
function addTodo(title) {
todos.value.push({
id: Date.now(),
title,
completed: false,
createdAt: new Date().toISOString()
})
}
function toggleTodo(id) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}
function deleteTodo(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
// 本地存储
onMounted(() => {
const saved = localStorage.getItem('todos')
if (saved) todos.value = JSON.parse(saved)
})
watch(todos, (val) => {
localStorage.setItem('todos', JSON.stringify(val))
}, { deep: true })
return {
todos,
filter,
filteredTodos,
activeCount,
completedCount,
addTodo,
toggleTodo,
deleteTodo
}
}
// App.vue 中使用
import { useTodos } from './composables/useTodos'
const {
todos,
filter,
filteredTodos,
activeCount,
completedCount,
addTodo,
toggleTodo,
deleteTodo
} = useTodos()
最佳实践总结
组件设计原则
| 原则 | 说明 |
|---|---|
| 单一职责 | 每个组件只做一件事 |
| 高内聚低耦合 | 相关功能放在一起,减少组件间依赖 |
| 可复用性 | 设计时考虑能否在其他地方使用 |
| 可测试性 | 易于编写单元测试 |
Props 设计
✅ 推荐
- 使用描述性的名称:todoTitle 而不是 data
- 提供合理的默认值
- 明确类型和必填性
- 避免传递过多 props(考虑拆分子组件)
❌ 避免
- 模糊的命名:item, data, obj
- 不指定类型
- 直接修改 prop 值
- Props 超过 5 个(考虑用对象传递或拆分)
事件设计
✅ 推荐
- 使用动词命名:add, delete, update
- 传递必要的数据,不要过多
- 保持事件语义清晰
❌ 避免
- 在事件中传递整个组件实例
- 一个事件做太多事情
- 事件名称过于模糊(如 change, click)
性能优化
| 技巧 | 场景 |
|---|---|
v-show vs v-if |
频繁切换用 v-show,条件渲染用 v-if |
computed 缓存 |
复杂计算使用计算属性而非方法 |
key 属性 |
列表渲染时提供唯一 key,避免使用 index |
| 懒加载 | 大组件使用 defineAsyncComponent |
| 虚拟滚动 | 长列表使用 vue-virtual-scroller |
shallowRef |
大型对象/数组,不需要深度响应式时 |
代码组织
✅ 推荐的组件内部结构
1. <script setup> 中的顺序:
- imports
- defineProps / defineEmits / defineModel
- 响应式状态 (ref, reactive)
- 计算属性 (computed)
- 侦听器 (watch)
- 生命周期钩子 (onMounted, etc.)
- 方法函数
- defineExpose
2. <template> 中的顺序:
- 注释分隔不同区块
- 主要容器
- 条件渲染的内容
- 列表渲染的内容
3. <style> 中的顺序:
- 布局相关
- 主题相关
- 动画相关
- 响应式媒体查询
常见陷阱
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 直接修改 prop | 违反单向数据流 | 使用 emit 通知父组件 |
| 用 index 作为 key | 列表重排时出错 | 使用唯一 id |
| 忘记 deep: true | 监听不到对象内部变化 | 对对象/数组使用深度监听 |
| 计算属性做副作用 | 违反纯函数原则 | 改用 watch |
| 过度使用事件总线 | 数据流难以追踪 | 优先 Props/Provide |
结语
Vue 自定义组件是构建现代前端应用的基石。通过本文的学习,你应该掌握了:
- 组件的基本结构和注册方式
- Props 和 Emits 的双向通信机制
- 模板语法的实战技巧
- 计算属性和侦听器的使用场景
- 组件通信的多种模式
- 样式隔离的最佳实践
- 组合式函数的复用模式
记住,好的组件设计是可复用、可测试、易维护的。在实际项目中,多思考组件的职责边界,保持接口的简洁清晰。
本文通过一个完整的待办事项应用示例,展示了组件化开发在真实场景中的应用。建议你动手实践,从简单的组件开始,逐步构建复杂的应用。
参考资料
作者:Work Hard Work Smart
出处:http://www.cnblogs.com/linlf03/
欢迎任何形式的转载,未经作者同意,请保留此段声明!
浙公网安备 33010602011771号