前言
Vue.js 是国内前端开发者最熟悉的框架之一。2020年9月发布的 Vue 3 带来了翻天覆地的变化——全新的 Composition API、基于 Proxy 的响应式系统、更好的 TypeScript 支持,以及性能上的大幅提升。
本文将从一个 Vue 2 开发者的视角,带你全面了解 Vue 3 的核心变化,并通过大量代码示例帮助你快速上手。无论你是刚接触 Vue 的新手,还是从 Vue 2 迁移的老手,这篇文章都能帮你建立起 Vue 3 的知识框架。
Vue 3 从发布至今已经经历了多个迭代版本,目前是构建用户界面的首选框架之一。它的核心理念始终保持不变:渐进式框架——你可以根据自己的项目需求,选择性地使用它的不同功能模块。这意味着你可以从一个简单的 CDN 脚本开始,逐步扩展到完整的工程化项目,这一点对初学者非常友好。
在开始之前,建议你使用 Vite 作为构建工具来创建 Vue 3 项目,它比 Webpack 快十倍以上:
# 创建 Vue 3 + TypeScript 项目
npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install
npm run dev
本文所有的代码示例都可以在这样一个新创建的项目中直接运行。好了,让我们正式开始 Vue 3 的学习之旅吧!
一、Options API vs Composition API:思维模式的转变
1.1 Options API 的痛点
在 Vue 2 中,我们习惯用 options(选项)来组织组件逻辑:
// Vue 2 Options API
export default {
data() {
return {
count: 0,
message: 'Hello',
users: [] as User[],
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
},
async fetchUsers() {
const res = await api.getUsers()
this.users = res.data
}
},
mounted() {
this.fetchUsers()
}
}
当组件变得复杂时,同一个功能的代码(比如 count 的相关逻辑)散落在 data、computed、methods、watch 各个选项中,维护起来非常痛苦。这就是我们常说的碎片化问题。
1.2 Composition API 的解决思路
Vue 3 的 Composition API 允许我们按功能组织代码,而不是按选项类型:
// Vue 3 Composition API
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// 计数器功能 —— 所有相关代码在一起
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
// 用户列表功能 —— 独立且内聚
const users = ref<User[]>([])
async function fetchUsers() {
const res = await api.getUsers()
users.value = res.data
}
onMounted(fetchUsers)
return { count, doubleCount, increment, users }
}
}
更有用的是,我们可以把逻辑提取到独立的函数中(后面会讲 Composables)。
1.3 <script setup> 语法糖
Vue 3.2 引入了 <script setup>,让 Composition API 的写法更加简洁:
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
const users = ref<User[]>([])
async function fetchUsers() {
const res = await api.getUsers()
users.value = res.data
}
onMounted(fetchUsers)
</script>
在
<script setup>中,顶层变量和函数可以直接在模板中使用,不再需要return。
二、响应式原理:从 Object.defineProperty 到 Proxy
2.1 Vue 2 的局限性
Vue 2 使用 Object.defineProperty 实现响应式,存在几个硬伤:
- 无法检测属性的新增和删除——必须用
Vue.set()/Vue.delete() - 无法直接检测数组索引和长度变化——必须用
splice等变异方法 - 初始化时递归遍历所有属性——对象越深,性能开销越大
// Vue 2 的痛点
const vm = new Vue({
data: { user: { name: 'Alice' } }
})
vm.user.age = 25 // ❌ 不是响应式的
vm.user = { name: 'Bob' } // ❌ 新对象丢失响应
delete vm.user.name // ❌ 不是响应式的
vm.arr[0] = 'x' // ❌ 数组索引不响应
2.2 Vue 3 的 Proxy 方案
Vue 3 改用 Proxy 代理整个对象,从根本上解决了上述问题。Proxy 是 ES6 中引入的特性,它可以在目标对象之外构建一层"拦截层",所有对对象的操作都会先经过这层拦截。这意味着我们可以捕获到属性的读取、赋值、删除、枚举等各种操作,从而实现全面的响应式追踪。
简单来说,Object.defineProperty 是在对象已有的属性上"动手脚",而 Proxy 是在对象外面"罩了一层网",任何操作都逃不过这张网。这也是为什么 Vue 3 能够检测到新增属性和数组索引变更的根本原因。
下面是一个 Proxy 响应式的简化实现示意:
// Vue 3 Proxy 响应式简化示意
const handler = {
get(target, key, receiver) {
track(target, key) // 依赖收集
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key)
return result
}
}
优势明显:
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 新增/删除属性 | ❌ 需 Vue.set/delete |
✅ 自动追踪 |
| 数组索引变更 | ❌ 不响应 | ✅ 自动追踪 |
| 数组 length 变化 | ❌ 不响应 | ✅ 自动追踪 |
| 初始化性能 | 递归遍历所有层级 | 懒递归,按需代理 |
| 支持 Map/Set | ❌ | ✅ |
// Vue 3 —— 一切皆响应
import { reactive } from 'vue'
const user = reactive({ name: 'Alice' })
user.age = 25 // ✅ 自动响应式
user.address = {} // ✅ 新属性也是响应式的
delete user.name // ✅ 触发视图更新
const list = reactive([1, 2, 3])
list[0] = 99 // ✅ 数组索引赋值
list.length = 1 // ✅ length 变更
三、ref / reactive / computed / watch 完整讲解
3.1 ref() —— 包装基本类型
ref 用于包装基本类型(number、string、boolean)或单个值,返回一个带有 .value 属性的响应式对象:
import { ref } from 'vue'
const count = ref(0)
const name = ref('Vue 3')
const isReady = ref(false)
// 读取和修改需要 .value
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
为什么需要
.value? 因为 JavaScript 基本类型是按值传递的,不包装成对象就无法追踪引用。而在模板中,Vue 会自动解包ref,所以不需要写.value。
ref 也支持传入复杂类型,但内部实际上会调用 reactive 来处理:
const state = ref({ count: 0, list: [1, 2, 3] })
state.value.count++ // 深层也是响应式的
state.value.list.push(4)
3.2 reactive() —— 包装对象
reactive 用于包装对象(包括数组、Map、Set),返回一个深层的响应式代理:
import { reactive } from 'vue'
interface Todo {
id: number
title: string
done: boolean
}
const state = reactive({
todos: [] as Todo[],
filter: 'all' as 'all' | 'active' | 'completed',
searchQuery: '',
})
// 直接操作,无需 .value
state.todos.push({ id: 1, title: 'Learn Vue 3', done: false })
state.filter = 'active'
ref vs reactive 怎么选?
| 场景 | 推荐 |
|---|---|
| 基本类型(number/string/boolean) | ref |
| 对象/数组,需要重新赋值 | ref(obj.value = newObj) |
| 深层嵌套对象,直接修改属性 | reactive |
| 从组合式函数返回 | ref(解构不丢失响应) |
⚠️ 重要提示:
reactive的对象如果解构或赋值给新变量,会丢失响应性。此时要用toRefs或转用ref。
const state = reactive({ x: 1, y: 2 })
// ❌ 丢失响应性
const { x, y } = state
// ✅ 保持响应性
const { x, y } = toRefs(state)
// 或者直接用 state.x
3.3 computed() —— 派生状态
computed 基于其他响应式数据计算出新的值,有缓存特性:
import { ref, computed } from 'vue'
const todos = ref([
{ id: 1, title: 'A', done: false },
{ id: 2, title: 'B', done: true },
])
// 只读计算属性
const activeTodos = computed(() =>
todos.value.filter(t => !t.done)
)
// 可读写计算属性(传入 getter + setter)
const allDone = computed({
get: () => todos.value.every(t => t.done),
set: (value: boolean) => {
todos.value.forEach(t => { t.done = value })
}
})
allDone.value = true // 全部标记完成
console.log(allDone.value) // true
3.4 watch() —— 侦听变化
Vue 3 的 watch 支持侦听单个或多个数据源:
import { ref, watch, reactive } from 'vue'
const count = ref(0)
const name = ref('Vue')
// 侦听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`)
})
// 侦听 reactive 对象的某个属性(需要传 getter 函数)
const state = reactive({ x: 0, y: 0 })
watch(
() => state.x,
(val) => console.log('x changed:', val)
)
// 侦听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log('count or name changed')
})
// 深度侦听
watch(
() => state,
(newVal) => console.log('state changed'),
{ deep: true, immediate: true }
)
watchEffect —— 自动追踪依赖:
import { ref, watchEffect } from 'vue'
const keyword = ref('')
// 回调中使用了哪些响应式数据,就自动侦听哪些
watchEffect(() => {
// 自动追踪 keyword.value
console.log(`搜索关键词: ${keyword.value}`)
})
watchEffect 与 watch 的区别:
| watch | watchEffect |
|---|---|
| 需明确指定侦听源 | 自动追踪回调中的依赖 |
| 可获取旧值 | 只能获取新值 |
| 懒执行(默认) | 立即执行一次 |
支持 immediate / deep 等选项 |
简洁但配置少 |
四、生命周期钩子的变化
Vue 3 的生命周期钩子全面重命名,同时 Composition API 提供了对应的函数形式:
| Vue 2 (Options API) | Vue 3 (Options API) | Vue 3 (Composition API) |
|---|---|---|
beforeCreate |
beforeCreate |
setup() |
created |
created |
setup() |
beforeMount |
beforeMount |
onBeforeMount |
mounted |
mounted |
onMounted |
beforeUpdate |
beforeUpdate |
onBeforeUpdate |
updated |
updated |
onUpdated |
beforeDestroy |
beforeUnmount |
onBeforeUnmount |
destroyed |
unmounted |
onUnmounted |
errorCaptured |
errorCaptured |
onErrorCaptured |
| — | renderTracked |
onRenderTracked |
| — | renderTriggered |
onRenderTriggered |
<script setup lang="ts">
import {
onMounted,
onUnmounted,
onBeforeMount,
onUpdated,
onErrorCaptured,
} from 'vue'
onBeforeMount(() => {
console.log('组件挂载前')
})
onMounted(() => {
console.log('组件已挂载')
window.addEventListener('resize', handleResize)
})
onUpdated(() => {
console.log('组件更新')
})
onUnmounted(() => {
console.log('组件已卸载')
window.removeEventListener('resize', handleResize)
})
onErrorCaptured((err) => {
console.error('子组件异常:', err)
return false // 阻止错误继续传播
})
</script>
🧠 在
setup()/<script setup>中,beforeCreate和created不需要显式调用——setup()本身就是围绕这两个钩子运行的。
五、模板语法与常用指令
5.1 插值与绑定
<template>
<!-- 文本插值 -->
<p>{{ message }}</p>
<!-- v-bind 绑定属性(简写 :) -->
<img :src="imageUrl" :alt="altText" />
<button :disabled="isLoading">提交</button>
<!-- class / style 增强绑定 -->
<div :class="['box', { active: isActive }]">Class 绑定</div>
<div :style="{ color: themeColor, fontSize: '14px' }">Style 绑定</div>
</template>
5.2 条件渲染:v-if / v-show
<template>
<!-- v-if 系列:真正的条件渲染 -->
<div v-if="status === 'loading'">加载中...</div>
<div v-else-if="status === 'error'">加载失败</div>
<div v-else>加载完成,共 {{ items.length }} 条</div>
<!-- v-show:仅切换 display 属性 -->
<div v-show="isVisible">始终渲染,仅切换 CSS</div>
</template>
v-if是惰性的(条件为假时不渲染元素),v-show总是渲染但切换 CSS display。
5.3 列表渲染:v-for
<template>
<!-- 遍历数组 -->
<ul>
<li v-for="(todo, index) in todos" :key="todo.id">
{{ index + 1 }}. {{ todo.title }}
</li>
</ul>
<!-- 遍历对象 -->
<div v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }}: {{ value }}
</div>
<!-- 遍历数字范围 -->
<span v-for="n in 10" :key="n">{{ n }}</span>
</template>
⚠️ 永远给
v-for提供:key,帮助 Vue 高效复用和重排 DOM 节点。
5.4 双向绑定:v-model
Vue 3 的 v-model 有了重大改进,支持多个 v-model 和自定义修饰符:
<template>
<!-- 基本用法 -->
<input v-model="search" />
<!-- 指定属性名(默认是 modelValue) -->
<input v-model:title="pageTitle" />
<!-- 多个 v-model -->
<UserForm
v-model:name="userName"
v-model:email="userEmail"
v-model:age="userAge"
/>
<!-- 自定义修饰符 -->
<input v-model.capitalize="text" />
</template>
子组件接收多个 v-model:
<!-- UserForm.vue -->
<script setup lang="ts">
defineProps<{
name: string
email: string
age: number
}>()
const emit = defineEmits<{
'update:name': [value: string]
'update:email': [value: string]
'update:age': [value: number]
}>()
</script>
六、组件通信方式
6.1 Props —— 父传子
<!-- Parent.vue -->
<script setup lang="ts">
import Child from './Child.vue'
const user = { name: 'Alice', age: 25 }
</script>
<template>
<Child title="Hello" :user="user" />
</template>
<!-- Child.vue -->
<script setup lang="ts">
// 方式一:类型声明
defineProps<{
title: string
user: { name: string; age: number }
}>()
// 方式二:运行时声明(支持默认值)
defineProps({
title: { type: String, required: true },
user: { type: Object, default: () => ({ name: '', age: 0 }) },
})
</script>
<template>
<h2>{{ title }}</h2>
<p>{{ user.name }},{{ user.age }}岁</p>
</template>
6.2 Emit —— 子传父
<!-- Child.vue -->
<script setup lang="ts">
const emit = defineEmits<{
confirm: [message: string]
cancel: []
}>()
function handleConfirm() {
emit('confirm', '用户确认了操作')
}
</script>
<template>
<button @click="handleConfirm">确认</button>
<button @click="emit('cancel')">取消</button>
</template>
<!-- Parent.vue -->
<template>
<Child @confirm="onConfirm" @cancel="onCancel" />
</template>
6.3 Slots —— 内容分发
<!-- Card.vue -->
<template>
<div class="card">
<!-- 默认插槽 -->
<div class="card-body">
<slot />
</div>
<!-- 具名插槽 -->
<div class="card-header">
<slot name="header" />
</div>
<!-- 作用域插槽:子传数据给父 -->
<div class="card-footer">
<slot name="footer" :count="items.length">
<!-- 默认内容 -->
<span>共 {{ items.length }} 项</span>
</slot>
</div>
</div>
</template>
<!-- 使用 Card -->
<template>
<Card>
<template #header>
<h2>标题</h2>
</template>
<p>正文内容</p>
<template #footer="{ count }">
<span>总计: {{ count }} 项</span>
</template>
</Card>
</template>
6.4 Provide / Inject —— 跨层级通信
适用于祖孙组件或深层嵌套的场景,避免 props 层层传递:
<!-- Ancestor.vue -->
<script setup lang="ts">
import { ref, provide } from 'vue'
const theme = ref<'light' | 'dark'>('light')
const user = ref({ name: 'Admin', role: 'admin' })
// 提供数据
provide('theme', theme)
provide('user', user)
</script>
<!-- DeepChild.vue -->
<script setup lang="ts">
import { inject } from 'vue'
// 注入数据(支持默认值和类型)
const theme = inject('theme', ref('light'))
const user = inject('user', ref({ name: '', role: '' }))
// 孙组件可以直接修改(如果允许)
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
<template>
<p>当前主题: {{ theme }}</p>
<p>当前用户: {{ user.name }} ({{ user.role }})</p>
<button @click="toggleTheme">切换主题</button>
</template>
✅ 使用
Symbol作为注入 key 可以避免命名冲突:export const THEME_KEY = Symbol('theme') provide(THEME_KEY, theme)
七、组合式函数(Composables):逻辑复用的最佳实践
Composables 是 Vue 3 中取代 Mixins 的逻辑复用方案。它以 use 开头的函数封装有状态的逻辑。
7.1 基础示例:useMouse
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
在组件中使用:
<script setup lang="ts">
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>鼠标位置: ({{ x }}, {{ y }})</p>
</template>
7.2 带参数的 Composables:useCounter
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
function decrement() { count.value-- }
function reset() { count.value = initialValue }
return { count, double, increment, decrement, reset }
}
7.3 异步 Composables:useFetch
// composables/useFetch.ts
import { ref, type Ref } from 'vue'
export function useFetch<T>(url: string | Ref<string>) {
const data = ref<T | null>(null)
const error = ref<string | null>(null)
const loading = ref(false)
async function execute() {
loading.value = true
error.value = null
try {
const target = typeof url === 'string' ? url : url.value
const res = await fetch(target)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e: any) {
error.value = e.message
} finally {
loading.value = false
}
}
execute()
return { data, error, loading, refresh: execute }
}
7.4 Composables 的使用原则
在实际开发中,编写 Composables 时有一些值得遵守的"最佳实践":
- 命名以
use开头 —— 这是一个约定俗成的规范,让其他开发者一眼就知道这是一个组合式函数 - 返回值为对象,不要使用数组 —— 对象解构更清晰,且避免了顺序依赖
- 接收 ref 参数时,用
toValue统一处理 —— 保证函数既支持普通值也支持 ref - 善用
watchEffect和watch处理副作用 —— 自动清理是 Composition API 的一大优势 - 不要修改传入的参数 —— 保持函数的纯粹性和可预测性
- 优先返回 ref 而非 reactive —— ref 在解构后不会丢失响应性,更灵活
// 好的实践:接收 ref 或普通值
import { toValue, type MaybeRef } from 'vue'
export function useGreeting(name: MaybeRef<string>) {
const greeting = computed(() => `你好, ${toValue(name)}!`)
return { greeting }
}
// 使用时既支持普通值也支持 ref
const { greeting } = useGreeting('Alice')
const nameRef = ref('Bob')
const { greeting: greeting2 } = useGreeting(nameRef)
7.5 Composables vs Mixins:为什么 Composables 更好?
在 Vue 2 时代,Mixins 是主要的逻辑复用手段,但它有很多问题。让我们对比一下:
| 对比维度 | Mixins | Composables |
|---|---|---|
| 命名冲突 | ❌ 多个 mixin 的属性名可能冲突,Vue 的合并策略会覆盖 | ✅ 解构时可以任意重命名,不存在冲突 |
| 来源清晰 | ❌ 模板中的属性不知道来自哪个 mixin,只能翻代码 | ✅ 显式调用函数,每个变量都知道来源 |
| 循环依赖 | ❌ 不支持 | ✅ 函数之间可以互相调用组合 |
| 类型推断 | ❌ 很难做 TypeScript 类型推导 | ✅ 天然支持完整的 TypeScript 泛型和类型推断 |
| Tree-shaking | ❌ 即使只用 mixin 的一部分,整个 mixin 也会被打包 | ✅ 只打包用到的函数,按需引入 |
从 Mixins 迁移到 Composables 并不难,本质上就是把 mixin 的选项对象改成一个函数,把 data 变成 ref,把 methods 变成普通函数,把 mounted 变成 onMounted。这样既保留了逻辑复用的能力,又避免了 Mixins 带来的各种问题。
八、实战:待办事项应用
现在我们把学到的知识整合起来,实现一个完整的待办事项应用。
8.1 Composables
// composables/useTodos.ts
import { ref, computed, watch } from 'vue'
export type FilterType = 'all' | 'active' | 'completed'
export interface Todo {
id: number
title: string
done: boolean
createdAt: Date
}
const STORAGE_KEY = 'vue3-todos'
export function useTodos() {
// 从 localStorage 恢复数据
const todos = ref<Todo[]>(loadTodos())
const filter = ref<FilterType>('all')
const searchQuery = ref('')
// 持久化
watch(todos, (val) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(val))
}, { deep: true })
// 计算属性
const filteredTodos = computed(() => {
let list = todos.value
if (searchQuery.value) {
const q = searchQuery.value.toLowerCase()
list = list.filter(t => t.title.toLowerCase().includes(q))
}
switch (filter.value) {
case 'active':
return list.filter(t => !t.done)
case 'completed':
return list.filter(t => t.done)
default:
return list
}
})
const activeCount = computed(() =>
todos.value.filter(t => !t.done).length
)
// 操作方法
function addTodo(title: string) {
const trimmed = title.trim()
if (!trimmed) return
todos.value.push({
id: Date.now(),
title: trimmed,
done: false,
createdAt: new Date(),
})
}
function toggleTodo(id: number) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
function removeTodo(id: number) {
todos.value = todos.value.filter(t => t.id !== id)
}
function clearCompleted() {
todos.value = todos.value.filter(t => !t.done)
}
return {
todos,
filter,
searchQuery,
filteredTodos,
activeCount,
addTodo,
toggleTodo,
removeTodo,
clearCompleted,
}
}
function loadTodos(): Todo[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
return raw ? JSON.parse(raw) : []
} catch {
return []
}
}
8.2 App.vue
<script setup lang="ts">
import { useTodos } from './composables/useTodos'
import TodoItem from './TodoItem.vue'
const {
filter,
searchQuery,
filteredTodos,
activeCount,
addTodo,
toggleTodo,
removeTodo,
clearCompleted,
} = useTodos()
const newTodoTitle = ref('')
function handleSubmit() {
addTodo(newTodoTitle.value)
newTodoTitle.value = ''
}
</script>
<template>
<div class="todo-app">
<h1>📋 待办事项</h1>
<!-- 输入框 -->
<form @submit.prevent="handleSubmit" class="input-area">
<input
v-model="newTodoTitle"
placeholder="输入新待办..."
class="todo-input"
/>
<button type="submit" class="btn-add">添加</button>
</form>
<!-- 搜索 & 筛选 -->
<div class="toolbar">
<input v-model="searchQuery" placeholder="搜索..." class="search-input" />
<select v-model="filter" class="filter-select">
<option value="all">全部</option>
<option value="active">未完成</option>
<option value="completed">已完成</option>
</select>
</div>
<!-- 待办列表 -->
<ul class="todo-list">
<TodoItem
v-for="todo in filteredTodos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo(todo.id)"
@remove="removeTodo(todo.id)"
/>
</ul>
<!-- 统计信息 -->
<div class="footer">
<span>{{ activeCount }} 项未完成</span>
<button
v-if="filteredTodos.some(t => t.done)"
@click="clearCompleted"
class="btn-clear"
>
清除已完成
</button>
</div>
</div>
</template>
8.3 TodoItem.vue
<script setup lang="ts">
import type { Todo } from './composables/useTodos'
defineProps<{
todo: Todo
}>()
const emit = defineEmits<{
toggle: []
remove: []
}>()
</script>
<template>
<li class="todo-item" :class="{ completed: todo.done }">
<input type="checkbox" :checked="todo.done" @change="emit('toggle')" />
<span class="todo-title">{{ todo.title }}</span>
<button @click="emit('remove')" class="btn-delete">🗑</button>
</li>
</template>
<style scoped>
.todo-item.completed .todo-title {
text-decoration: line-through;
color: #999;
}
</style>
💡 完整的样式代码可以自由发挥,以上是核心逻辑部分。
九、总结
Vue 3 并不是对 Vue 2 的推翻重来,而是一次渐进式的进化。你可以:
- 继续使用 Options API(Vue 3 完全兼容)
- 在新组件中逐步尝试 Composition API
- 用
<script setup>获得最简洁的开发体验 - 用 Composables 替代 Mixins 实现优雅的逻辑复用
- 享受 Proxy 带来的完整响应式能力
学习建议:
- 先掌握
ref/reactive/computed/watch四个核心 API - 理解
setup()的时机和生命周期 - 从小功能开始提取 Composables
- 多看官方文档的示例和 TypeScript 类型推断
Vue 3 的生态已经非常成熟(Vite、Pinia、Vue Router 4、Nuxt 3 等),现在正是全面学习和使用 Vue 3 的最佳时机。
本文作者是一名热爱分享的前端开发者。如果你觉得文章有帮助,欢迎点赞和收藏,也欢迎在评论区讨论你在学习 Vue 3 过程中遇到的问题!
评论