引言
我认识一个朋友,之前写了两年Vue2,最近开始用Vue3重构项目。第一次看到Composition API的时候,他发了条朋友圈:”这不就是把options API拆开写吗?”
说实话,我一开始也是这么想的。但当我用它写了两个实际项目之后,发现这玩意儿确实香。
为什么?因为以前写组件,所有逻辑都堆在data、methods、computed、watch这四个选项里。同一个功能相关的代码被拆得七零八落,找起来头疼,改起来更头疼。Composition API把同一个功能的代码聚在一起,逻辑清晰多了。
这篇文章就是把我从”看不懂Composition API”到”真香”的过程记录下来,帮你少走弯路。
环境准备:搭建Vue3项目
使用Vite创建项目
Vite是Vue官方推荐的构建工具,比Vue CLI快很多。
bash
# 创建项目
npm create vite@latest my-vue-app -- --template vue
# 进入项目目录
cd my-vue-app
# 安装依赖
npm install
# 启动开发服务器
npm run dev
项目结构大概是这样的:
plaintext
my-vue-app/
├── src/
│ ├── assets/
│ ├── components/
│ ├── App.vue
│ └── main.js
├── index.html
├── package.json
└── vite.config.js
Vue3组件的基本结构
打开App.vue,你会看到这样的代码:
vue
<script setup>
import { ref, computed, onMounted } from 'vue'
// 响应式数据
const count = ref(0)
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log('组件挂载完成')
})
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<p>双倍:{{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>
<style scoped>
button {
padding: 8px 16px;
background: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
这就是Vue3 Composition API的基本写法。注意<script setup>语法,这是官方推荐的写法,比普通<script>更简洁。
响应式系统:Vue3的核心
响应式是Vue的灵魂。数据变化,界面自动更新,不需要手动操作DOM。
ref和reactive
Vue3有两种创建响应式数据的方式:
ref:基础类型的响应式
vue
<script setup>
import { ref } from 'vue'
// ref用于基础类型(string、number、boolean等)
const count = ref(0)
const name = ref('小明')
const isActive = ref(false)
// 访问值用.value
function increment() {
count.value++
}
function changeName() {
name.value = '小红'
}
</script>
<template>
<div>
<p>{{ name }} 点击了 {{ count }} 次</p>
<button @click="increment">点我</button>
<p v-if="isActive">显示中</p>
</div>
</template>
reactive:对象的响应式
vue
<script setup>
import { reactive } from 'vue'
// reactive用于对象和数组
const user = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
})
function birthday() {
user.age++
}
function updateEmail() {
user.email = 'new_' + user.email
}
</script>
<template>
<div>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<p>邮箱:{{ user.email }}</p>
<button @click="birthday">过生日</button>
</div>
</template>
什么时候用ref,什么时候用reactive?
我的经验是:
基础类型用ref
对象、数组用reactive
或者统一用ref,因为ref也可以包装对象:const user = ref({...})
深度响应式
默认情况下,ref和reactive都是深度响应式的:
vue
<script setup>
import { ref, reactive } from 'vue'
const obj = reactive({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 这些修改都会触发响应式更新
obj.nested.count++
obj.arr.push('baz')
}
const nestedRef = ref({
value: { count: 0 }
})
function mutateNestedRef() {
// ref包装的对象,修改时需要用.value
nestedRef.value.count++
}
</script>
computed计算属性
计算属性根据其他响应式数据计算得出,有缓存特性:
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 只读计算属性
const fullName = computed(() => {
return `${lastName.value}${firstName.value}`
})
// 可写计算属性
const fullNameWritable = computed({
get: () => `${lastName.value}${firstName.value}`,
set: (value) => {
// "李四" -> lastName="李", firstName="四"
if (value.length >= 2) {
lastName.value = value[0]
firstName.value = value.slice(1)
}
}
})
function updateName() {
fullNameWritable.value = '王五'
}
</script>
<template>
<div>
<p>姓名:{{ fullName }}</p>
<button @click="updateName">改为王五</button>
</div>
</template>
watch监听器
当响应式数据变化时执行某些操作:
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('')
// 监听单个响应式数据
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
answer.value = '思考中...'
// 模拟异步请求
setTimeout(() => {
answer.value = '这是我的答案'
}, 1000)
}
})
// 监听多个数据
const first = ref('')
const second = ref('')
watch([first, second], ([newFirst, newSecond], [oldFirst, oldSecond]) => {
console.log(`从 ${oldFirst},${oldSecond} 变为 ${newFirst},${newSecond}`)
})
</script>
<template>
<div>
<input v-model="question" placeholder="输入问题..." />
<p>{{ answer }}</p>
</div>
</template>
watchEffect是另一个选择,它会自动追踪依赖:
vue
<script setup>
import { ref, watchEffect } from 'vue'
const url = ref('https://api.example.com/data')
watchEffect(async () => {
// 这里的代码会自动追踪url.value的变化
const response = await fetch(url.value)
const data = await response.json()
console.log('获取到数据:', data)
})
// 改变url会自动触发上面的代码重新执行
</script>
生命周期钩子
每个组件从创建到销毁会经历多个阶段,生命周期钩子让你在各个阶段执行代码。
vue
<script setup>
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount,
onActivated,
onDeactivated,
onErrorCaptured
} from 'vue'
// 组件挂载前
onBeforeMount(() => {
console.log('组件即将挂载')
})
// 组件挂载后(DOM已创建)
onMounted(() => {
console.log('组件已挂载')
// 适合:获取DOM、绑定事件、发送请求
})
// 组件更新前
onBeforeUpdate(() => {
console.log('组件即将更新')
})
// 组件更新后(DOM已更新)
onUpdated(() => {
console.log('组件已更新')
// 适合:获取更新后的DOM
})
// 组件卸载前
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
// 组件卸载后
onUnmounted(() => {
console.log('组件已卸载')
// 适合:清理定时器、取消订阅、解绑事件
})
// 错误捕获(Vue3新增)
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err)
console.error('错误信息:', info)
return false // 阻止错误继续传播
})
</script>
对比Vue2的选项和Vue3的Composition API写法:
Vue2选项 Vue3钩子函数 说明 beforeCreate – setup()替代 created – setup()替代 beforeMount onBeforeMount 挂载前 mounted onMounted 挂载后 beforeUpdate onBeforeUpdate 更新前 updated onUpdated 更新后 beforeDestroy onBeforeUnmount 卸载前 destroyed onUnmounted 卸载后
组合式函数:Composition API的精髓
组合式函数(Composables)是Composition API最强大的特性。它让你把相关逻辑抽离成可复用的函数。
什么是组合式函数
简单说,组合式函数就是一个用Composition API写的有复用价值的函数:
javascript
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
在组件中使用组合式函数
vue
<!-- Counter.vue -->
<script setup>
import { useCounter } from './useCounter'
// 在组件中使用
const { count, doubleCount, increment, decrement, reset } = useCounter(10)
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<p>双倍:{{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">重置</button>
</div>
</template>
现在任何组件想用计数器功能,只需调用这个函数,不需要继承、不需要mixin,代码清晰多了。
实战:封装一个请求数据的组合式函数
javascript
// useFetch.js
import { ref, watchEffect, isRef } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function doFetch() {
loading.value = true
error.value = null
try {
// 支持传入ref或普通字符串
const urlToFetch = isRef(url) ? url.value : url
const response = await fetch(urlToFetch)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// 如果url是ref,自动追踪变化并重新请求
if (isRef(url)) {
watchEffect(doFetch)
} else {
doFetch()
}
return { data, error, loading, refresh: doFetch }
}
使用这个函数:
vue
<script setup>
import { ref } from 'vue'
import { useFetch } from './useFetch'
// 直接传字符串
const { data: users, loading: usersLoading, error: usersError } = useFetch('/api/users')
// 传ref,当id变化时自动重新请求
const userId = ref('1')
const { data: userDetail, loading, error } = useFetch(
computed(() => `/api/users/${userId.value}`)
)
function changeUser() {
userId.value = String(parseInt(userId.value) + 1)
}
</script>
<template>
<div>
<button @click="changeUser">切换用户</button>
<div v-if="loading">加载中...</div>
<div v-else-if="error">出错了:{{ error.message }}</div>
<div v-else>
<p>用户详情:{{ userDetail }}</p>
</div>
</div>
</template>
实战:封装一个本地存储的组合式函数
javascript
// useStorage.js
import { ref, watch } from 'vue'
export function useStorage(key, defaultValue) {
// 从localStorage读取初始值
const storedValue = localStorage.getItem(key)
const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
// 监听变化,自动保存到localStorage
watch(data, (newValue) => {
if (newValue === null || newValue === undefined) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newValue))
}
}, { deep: true })
return data
}
使用:
vue
<script setup>
import { useStorage } from './useStorage'
// 第一个参数是localStorage的key
// 第二个参数是默认值
const username = useStorage('username', '')
const theme = useStorage('theme', 'light')
const preferences = useStorage('preferences', { fontSize: 14, language: 'zh' })
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
<template>
<div>
<input v-model="username" placeholder="输入用户名" />
<p>当前主题:{{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
<p>首选项:{{ preferences }}</p>
</div>
</template>
依赖注入:跨层级传递数据
props层层传递很麻烦?依赖注入帮你解决。
provide和inject
父组件提供数据,后代组件直接获取:
vue
<!-- Parent.vue -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('light')
const user = ref({ name: '张三', role: 'admin' })
// 提供给所有后代组件
provide('theme', theme)
provide('user', user)
// 也可以提供函数
provide('toggleTheme', () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
})
</script>
<template>
<div :class="theme">
<ChildComponent />
</div>
</template>
vue
<!-- GrandChild.vue(不需要父传子的props) -->
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('theme')
const user = inject('user')
const toggleTheme = inject('toggleTheme')
</script>
<template>
<div>
<p>当前主题:{{ theme }}</p>
<p>用户:{{ user.name }} ({{ user.role }})</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>
带默认值的注入
vue
<script setup>
import { inject } from 'vue'
// 第二个参数是默认值
const config = inject('config', { apiUrl: '/api', timeout: 5000 })
// 或者用函数提供默认值(适合引用类型)
const theme = inject('theme', () => ref('light'), true)
</script>
模板引用:操作DOM
虽然Vue推崇数据驱动DOM,但有些时候还是需要直接操作DOM元素。
ref绑定元素
vue
<script setup>
import { ref, onMounted } from 'vue'
// 创建ref
const inputRef = ref(null)
const canvasRef = ref(null)
onMounted(() => {
// 访问DOM元素
inputRef.value.focus()
// 操作Canvas
const ctx = canvasRef.value.getContext('2d')
ctx.fillStyle = 'green'
ctx.fillRect(0, 0, 100, 100)
})
</script>
<template>
<div>
<!-- 绑定ref -->
<input ref="inputRef" type="text" />
<canvas ref="canvasRef" width="200" height="200"></canvas>
</div>
</template>
ref绑定组件实例
vue
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
function getCount() {
return count.value
}
// 暴露方法给父组件
defineExpose({
count,
increment,
getCount
})
</script>
vue
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
function handleClick() {
console.log('子组件count:', childRef.value.count)
childRef.value.increment()
console.log('调用后count:', childRef.value.getCount())
}
</script>
<template>
<div>
<ChildComponent ref="childRef" />
<button @click="handleClick">操作子组件</button>
</div>
</template>
实战:Todo应用
学完上面的知识,来写个完整的Todo应用练练手。
项目结构
plaintext
src/
├── components/
│ ├── TodoList.vue
│ ├── TodoItem.vue
│ ├── TodoInput.vue
│ └── TodoFilters.vue
├── composables/
│ └── useTodos.js
├── App.vue
└── main.js
useTodos.js:封装Todo逻辑
javascript
// composables/useTodos.js
import { ref, computed, watch } from 'vue'
export function useTodos() {
// 状态
const todos = ref([])
const filter = ref('all') // all, active, completed
// 本地存储
const STORAGE_KEY = 'vue3-todos'
// 初始化时从localStorage读取
const storedTodos = localStorage.getItem(STORAGE_KEY)
if (storedTodos) {
try {
todos.value = JSON.parse(storedTodos)
} catch (e) {
console.error('读取本地存储失败:', e)
}
}
// 保存到localStorage
watch(todos, (newTodos) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newTodos))
}, { deep: true })
// 计算属性
const filteredTodos = computed(() => {
if (filter.value === 'active') {
return todos.value.filter(t => !t.completed)
} else if (filter.value === 'completed') {
return todos.value.filter(t => t.completed)
}
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) {
if (!title.trim()) return
todos.value.push({
id: Date.now(),
title: title.trim(),
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) {
const index = todos.value.findIndex(t => t.id === id)
if (index !== -1) {
todos.value.splice(index, 1)
}
}
function editTodo(id, newTitle) {
const todo = todos.value.find(t => t.id === id)
if (todo && newTitle.trim()) {
todo.title = newTitle.trim()
}
}
function clearCompleted() {
todos.value = todos.value.filter(t => !t.completed)
}
function setFilter(newFilter) {
filter.value = newFilter
}
return {
todos,
filter,
filteredTodos,
activeCount,
completedCount,
addTodo,
toggleTodo,
deleteTodo,
editTodo,
clearCompleted,
setFilter
}
}
TodoInput.vue:输入组件
vue
<script setup>
import { ref } from 'vue'
const emit = defineEmits(['add'])
const inputValue = ref('')
function handleSubmit() {
if (inputValue.value.trim()) {
emit('add', inputValue.value)
inputValue.value = ''
}
}
</script>
<template>
<form @submit.prevent="handleSubmit" class="todo-input">
<input
v-model="inputValue"
type="text"
placeholder="输入新任务..."
class="input-field"
/>
<button type="submit" class="add-btn">添加</button>
</form>
</template>
<style scoped>
.todo-input {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.input-field {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
}
.input-field:focus {
outline: none;
border-color: #42b883;
}
.add-btn {
padding: 12px 24px;
background: #42b883;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
}
.add-btn:hover {
background: #3aa876;
}
</style>
TodoItem.vue:单个任务组件
vue
<script setup>
import { ref } from 'vue'
const props = defineProps({
todo: {
type: Object,
required: true
}
})
const emit = defineEmits(['toggle', 'delete', 'edit'])
const isEditing = ref(false)
const editValue = ref('')
function startEdit() {
editValue.value = props.todo.title
isEditing.value = true
}
function saveEdit() {
if (editValue.value.trim()) {
emit('edit', props.todo.id, editValue.value)
}
isEditing.value = false
}
function cancelEdit() {
isEditing.value = false
}
</script>
<template>
<li class="todo-item" :class="{ completed: todo.completed }">
<div v-if="isEditing" class="edit-mode">
<input
v-model="editValue"
@keyup.enter="saveEdit"
@keyup.escape="cancelEdit"
class="edit-input"
/>
<button @click="saveEdit" class="save-btn">保存</button>
<button @click="cancelEdit" class="cancel-btn">取消</button>
</div>
<div v-else class="view-mode">
<input
type="checkbox"
:checked="todo.completed"
@change="$emit('toggle', todo.id)"
class="checkbox"
/>
<span class="title">{{ todo.title }}</span>
<button @click="startEdit" class="edit-btn">编辑</button>
<button @click="$emit('delete', todo.id)" class="delete-btn">删除</button>
</div>
</li>
</template>
<style scoped>
.todo-item {
display: flex;
align-items: center;
padding: 12px;
background: #f9f9f9;
border-radius: 6px;
margin-bottom: 8px;
}
.todo-item.completed .title {
text-decoration: line-through;
color: #999;
}
.checkbox {
width: 20px;
height: 20px;
margin-right: 12px;
cursor: pointer;
}
.title {
flex: 1;
font-size: 16px;
}
button {
padding: 6px 12px;
margin-left: 8px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.edit-btn {
background: #e0e0e0;
color: #333;
}
.delete-btn {
background: #ff4444;
color: white;
}
.edit-mode {
display: flex;
width: 100%;
}
.edit-input {
flex: 1;
padding: 8px;
border: 1px solid #42b883;
border-radius: 4px;
font-size: 16px;
}
.save-btn {
background: #42b883;
color: white;
margin-left: 8px;
}
.cancel-btn {
background: #e0e0e0;
color: #333;
}
</style>
TodoFilters.vue:筛选组件
vue
<script setup>
defineProps({
filter: {
type: String,
required: true
},
activeCount: {
type: Number,
required: true
},
completedCount: {
type: Number,
required: true
}
})
const emit = defineEmits(['setFilter', 'clearCompleted'])
const filters = [
{ key: 'all', label: '全部' },
{ key: 'active', label: '进行中' },
{ key: 'completed', label: '已完成' }
]
</script>
<template>
<div class="filters">
<div class="filter-buttons">
<button
v-for="f in filters"
:key="f.key"
@click="emit('setFilter', f.key)"
:class="{ active: filter === f.key }"
class="filter-btn"
>
{{ f.label }}
</button>
</div>
<div class="stats">
<span>进行中:{{ activeCount }}</span>
<span>已完成:{{ completedCount }}</span>
<button
v-if="completedCount > 0"
@click="emit('clearCompleted')"
class="clear-btn"
>
清除已完成
</button>
</div>
</div>
</template>
<style scoped>
.filters {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-top: 1px solid #eee;
}
.filter-buttons {
display: flex;
gap: 8px;
}
.filter-btn {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.filter-btn.active {
background: #42b883;
color: white;
border-color: #42b883;
}
.stats {
display: flex;
gap: 16px;
align-items: center;
font-size: 14px;
color: #666;
}
.clear-btn {
padding: 4px 8px;
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
</style>
TodoList.vue:整合所有组件
vue
<script setup>
import { useTodos } from '../composables/useTodos'
import TodoInput from './TodoInput.vue'
import TodoItem from './TodoItem.vue'
import TodoFilters from './TodoFilters.vue'
const {
filter,
filteredTodos,
activeCount,
completedCount,
addTodo,
toggleTodo,
deleteTodo,
editTodo,
clearCompleted,
setFilter
} = useTodos()
</script>
<template>
<div class="todo-list">
<h1 class="title">Vue3 Todo应用</h1>
<TodoInput @add="addTodo" />
<ul class="todos">
<TodoItem
v-for="todo in filteredTodos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo"
@delete="deleteTodo"
@edit="editTodo"
/>
</ul>
<div v-if="filteredTodos.length === 0" class="empty">
暂无任务
</div>
<TodoFilters
:filter="filter"
:active-count="activeCount"
:completed-count="completedCount"
@set-filter="setFilter"
@clear-completed="clearCompleted"
/>
</div>
</template>
<style scoped>
.todo-list {
max-width: 600px;
margin: 40px auto;
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.title {
text-align: center;
color: #42b883;
margin-bottom: 24px;
}
.todos {
list-style: none;
padding: 0;
margin: 0;
}
.empty {
text-align: center;
padding: 40px;
color: #999;
}
</style>
App.vue:主组件
vue
<script setup>
import TodoList from './components/TodoList.vue'
</script>
<template>
<div class="app">
<TodoList />
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.app {
min-height: 100vh;
padding: 20px;
}
</style>
运行效果:
bash
npm run dev
打开浏览器访问http://localhost:5173,一个完整的Todo应用就完成了。
总结
好了,这篇Vue3 Composition API教程就到这里。让我回顾一下今天学的内容:
环境搭建 :Vite创建Vue3项目,基本组件结构
响应式系统 :ref和reactive的区别,深度响应式
计算属性和监听器 :computed、watch、watchEffect
生命周期钩子 :从创建到销毁的各个阶段
组合式函数 :封装可复用的逻辑
依赖注入 :provide和inject跨层级传递数据
模板引用 :ref绑定DOM和组件
实战项目 :完整的Todo应用
说实话,Composition API确实比Options API更灵活、更强大。它让你能够:
把相关逻辑放在一起,不用在data、methods、computed之间跳来跳去
抽离出可复用的组合式函数,比mixin清晰多了
更好地组织TypeScript代码(虽然本教程没用TypeScript,但Vue3对TS的支持很好)
如果你之前用Vue2,可以渐进式迁移。新项目建议直接上Composition API,老项目可以逐步把新组件改成新写法。
关于内链方面,你可以继续学习TypeScript入门完全指南 ,TypeScript+Vue3是绝配。或者学习RESTful API接口设计规范 ,了解如何设计后端API配合Vue3前端使用。
常见问题
Q:Composition API和Options API哪个好?
A:Composition API更好。它让代码逻辑更集中,更容易复用,也更适合TypeScript。新项目建议用Composition API。
Q:需要完全放弃Vue2吗?
A:不需要。Vue3完全兼容Options API语法,老项目可以慢慢迁移。可以先在新组件使用Composition API,老组件保持Options API写法。
Q:什么时候用ref,什么时候用reactive?
A:建议统一用ref。reactive有一些坑(比如解构后失去响应式),ref没有这个问题,而且语法更一致。
Q:组合式函数为什么要用”use”开头?
A:这是一个约定俗成的命名方式,类似于React的”hooks”命名。它让代码更易读,一看就知道这是个组合式函数,可以自动追踪依赖。
Q:Vue3适合做大型项目吗?
A:非常适合。Composition API让代码更容易组织和复用,TypeScript支持也很好。我见过不少几百个组件的大型Vue3项目,代码依然保持很好的可维护性。
如果你有任何问题或建议,欢迎在评论区交流!