1. 初识Vue 3的setup函数第一次接触Vue 3的setup函数时我完全被它的简洁和强大震撼了。这个函数就像是组件逻辑的控制中心把所有相关的代码都集中在一个地方再也不用像以前那样在data、methods、computed之间来回跳转了。setup函数是Composition API的入口它在组件实例创建之前执行。这意味着在setup内部this是undefined——这个特性让我踩过不少坑但也让我更清楚地理解了Vue的生命周期。在实际项目中我发现setup特别适合处理复杂的业务逻辑因为它允许我们把代码按功能而不是选项类型来组织。举个例子假设我们要开发一个用户管理系统。在Options API时代用户数据可能分散在data里相关方法在methods中而计算属性又在computed里。现在有了setup我们可以把所有用户相关的逻辑放在一起import { ref, computed } from vue export default { setup() { // 用户数据 const users ref([]) const currentUser ref(null) // 用户相关方法 const fetchUsers async () { users.value await api.getUsers() } // 计算属性 const activeUsers computed(() { return users.value.filter(user user.isActive) }) return { users, currentUser, fetchUsers, activeUsers } } }这种组织方式让代码的可读性和可维护性大大提升。当需要修改某个功能时所有相关代码都在一个地方再也不用在文件中上下翻找了。2. setup函数的基础用法2.1 响应式数据管理在setup中我们主要使用ref和reactive来创建响应式数据。刚开始我经常纠结该用哪个后来总结出一个简单原则基础类型用ref对象类型用reactive。ref的使用很简单但要注意.value的问题。刚开始我经常忘记写.value导致各种奇怪的bug。后来发现一个技巧在模板中使用ref时不需要.valueVue会自动解包但在JavaScript代码中必须使用.value。const count ref(0) // 创建 count.value // 修改reactive则更适合处理复杂对象。但要注意解构reactive对象会失去响应性。这时候可以用toRefs来保持响应性const user reactive({ name: 张三, age: 25 }) // 错误解构会失去响应性 const { name, age } user // 正确做法 const { name, age } toRefs(user)2.2 生命周期钩子的使用Vue 3的生命周期钩子都变成了onXxx形式的函数可以直接在setup中使用。我最常用的是onMounted和onUnmounted用来处理组件的初始化和清理工作。import { onMounted, onUnmounted } from vue setup() { const timer ref(null) onMounted(() { timer.value setInterval(() { console.log(定时器运行中...) }, 1000) }) onUnmounted(() { clearInterval(timer.value) }) }这里有个小技巧生命周期钩子会自动绑定到当前组件实例所以不需要担心this的问题。而且多个相同钩子会按顺序执行这在拆分复杂逻辑时特别有用。3. setup函数的进阶技巧3.1 处理props和emitsetup函数的第一个参数是props它是响应式的——这意味着你可以用watch来监听props的变化。但要注意不能直接解构props否则会失去响应性。export default { props: [userId], setup(props) { // 监听userId变化 watch(() props.userId, (newId) { fetchUser(newId) }) } }第二个参数是context包含attrs、slots和emit。我特别喜欢用解构语法来获取emitsetup(props, { emit }) { const handleClick () { emit(click, 自定义事件) } return { handleClick } }3.2 组合式函数的提取与复用Composition API最强大的特性就是能够轻松提取和复用逻辑。我们可以把相关的响应式数据、计算属性和方法封装成一个组合式函数。比如我们可以把表单处理的逻辑提取出来// useForm.js import { ref } from vue export function useForm(initialValue) { const formData ref(initialValue) const errors ref({}) const validate () { // 验证逻辑... } const submit async () { if (validate()) { await api.submit(formData.value) } } return { formData, errors, submit } }然后在组件中使用import { useForm } from ./useForm setup() { const { formData, errors, submit } useForm({ name: , email: }) return { formData, errors, submit } }这种方式让我们的代码像乐高积木一样可以自由组合大大提高了代码的复用性。4. 实战构建待办事项应用4.1 项目结构与初始化让我们用setup函数构建一个完整的待办事项应用。首先创建项目结构todo-app/ ├── src/ │ ├── components/ │ │ ├── TodoList.vue │ │ └── TodoForm.vue │ ├── composables/ │ │ └── useTodos.js │ ├── App.vue │ └── main.js在main.js中初始化Vue应用import { createApp } from vue import App from ./App.vue createApp(App).mount(#app)4.2 核心功能实现首先在useTodos.js中创建待办事项的核心逻辑import { ref, computed } from vue export function useTodos() { const todos ref([]) const filter ref(all) // 添加待办事项 const addTodo (text) { todos.value.push({ id: Date.now(), text, completed: false }) } // 切换完成状态 const toggleTodo (id) { const todo todos.value.find(t t.id id) if (todo) { todo.completed !todo.completed } } // 过滤后的待办事项 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 } }) return { todos, filter, addTodo, toggleTodo, filteredTodos } }然后在App.vue中使用这个组合式函数template div classtodo-app h1待办事项/h1 TodoForm addaddTodo / TodoList :todosfilteredTodos toggletoggleTodo / div classfilters button clickfilter all全部/button button clickfilter active未完成/button button clickfilter completed已完成/button /div /div /template script import { useTodos } from ./composables/useTodos import TodoForm from ./components/TodoForm.vue import TodoList from ./components/TodoList.vue export default { components: { TodoForm, TodoList }, setup() { const { todos, filter, addTodo, toggleTodo, filteredTodos } useTodos() return { filter, addTodo, toggleTodo, filteredTodos } } } /script4.3 组件实现TodoForm.vue负责添加新待办事项template form submit.preventhandleSubmit input v-modelnewTodo placeholder输入待办事项... / button typesubmit添加/button /form /template script import { ref } from vue export default { emits: [add], setup(props, { emit }) { const newTodo ref() const handleSubmit () { if (newTodo.value.trim()) { emit(add, newTodo.value) newTodo.value } } return { newTodo, handleSubmit } } } /scriptTodoList.vue显示待办事项列表template ul li v-fortodo in todos :keytodo.id click$emit(toggle, todo.id) :class{ completed: todo.completed } {{ todo.text }} /li /ul /template script export default { props: [todos], emits: [toggle] } /script5. 常见问题与最佳实践5.1 避免的陷阱在使用setup函数时有几个常见的坑需要注意不要混合使用Options API和Composition API虽然技术上可行但会让代码难以维护。选择一种风格并坚持使用。异步setup的问题setup函数默认应该是同步的。如果需要异步操作可以在setup内部使用async/await但不能把setup本身声明为async函数。响应式丢失问题解构props或reactive对象时要小心必要时使用toRefs保持响应性。5.2 性能优化技巧合理使用computed对于需要复杂计算的派生数据使用computed可以缓存结果避免不必要的重复计算。使用shallowRef和shallowReactive当你知道某些数据不需要深度响应时可以使用这些浅层响应式API来提升性能。合理组织代码把不相关的逻辑拆分到不同的组合式函数中可以减少组件的重新渲染。5.3 测试策略测试setup-based组件其实更简单了因为逻辑都被封装在独立的函数中。我们可以直接测试组合式函数import { useTodos } from ./useTodos import { ref } from vue test(addTodo should add new todo, () { const todos ref([]) const { addTodo } useTodos(todos) addTodo(测试待办项) expect(todos.value.length).toBe(1) expect(todos.value[0].text).toBe(测试待办项) })对于组件测试Vue Test Utils提供了专门的方法来测试setup-based组件import { mount } from vue/test-utils import TodoForm from ./TodoForm.vue test(emits add event with new todo, async () { const wrapper mount(TodoForm) await wrapper.find(input).setValue(新任务) await wrapper.find(form).trigger(submit) expect(wrapper.emitted().add[0]).toEqual([新任务]) })
掌握Vue 3 Composition API的setup函数—从基础到实战应用
1. 初识Vue 3的setup函数第一次接触Vue 3的setup函数时我完全被它的简洁和强大震撼了。这个函数就像是组件逻辑的控制中心把所有相关的代码都集中在一个地方再也不用像以前那样在data、methods、computed之间来回跳转了。setup函数是Composition API的入口它在组件实例创建之前执行。这意味着在setup内部this是undefined——这个特性让我踩过不少坑但也让我更清楚地理解了Vue的生命周期。在实际项目中我发现setup特别适合处理复杂的业务逻辑因为它允许我们把代码按功能而不是选项类型来组织。举个例子假设我们要开发一个用户管理系统。在Options API时代用户数据可能分散在data里相关方法在methods中而计算属性又在computed里。现在有了setup我们可以把所有用户相关的逻辑放在一起import { ref, computed } from vue export default { setup() { // 用户数据 const users ref([]) const currentUser ref(null) // 用户相关方法 const fetchUsers async () { users.value await api.getUsers() } // 计算属性 const activeUsers computed(() { return users.value.filter(user user.isActive) }) return { users, currentUser, fetchUsers, activeUsers } } }这种组织方式让代码的可读性和可维护性大大提升。当需要修改某个功能时所有相关代码都在一个地方再也不用在文件中上下翻找了。2. setup函数的基础用法2.1 响应式数据管理在setup中我们主要使用ref和reactive来创建响应式数据。刚开始我经常纠结该用哪个后来总结出一个简单原则基础类型用ref对象类型用reactive。ref的使用很简单但要注意.value的问题。刚开始我经常忘记写.value导致各种奇怪的bug。后来发现一个技巧在模板中使用ref时不需要.valueVue会自动解包但在JavaScript代码中必须使用.value。const count ref(0) // 创建 count.value // 修改reactive则更适合处理复杂对象。但要注意解构reactive对象会失去响应性。这时候可以用toRefs来保持响应性const user reactive({ name: 张三, age: 25 }) // 错误解构会失去响应性 const { name, age } user // 正确做法 const { name, age } toRefs(user)2.2 生命周期钩子的使用Vue 3的生命周期钩子都变成了onXxx形式的函数可以直接在setup中使用。我最常用的是onMounted和onUnmounted用来处理组件的初始化和清理工作。import { onMounted, onUnmounted } from vue setup() { const timer ref(null) onMounted(() { timer.value setInterval(() { console.log(定时器运行中...) }, 1000) }) onUnmounted(() { clearInterval(timer.value) }) }这里有个小技巧生命周期钩子会自动绑定到当前组件实例所以不需要担心this的问题。而且多个相同钩子会按顺序执行这在拆分复杂逻辑时特别有用。3. setup函数的进阶技巧3.1 处理props和emitsetup函数的第一个参数是props它是响应式的——这意味着你可以用watch来监听props的变化。但要注意不能直接解构props否则会失去响应性。export default { props: [userId], setup(props) { // 监听userId变化 watch(() props.userId, (newId) { fetchUser(newId) }) } }第二个参数是context包含attrs、slots和emit。我特别喜欢用解构语法来获取emitsetup(props, { emit }) { const handleClick () { emit(click, 自定义事件) } return { handleClick } }3.2 组合式函数的提取与复用Composition API最强大的特性就是能够轻松提取和复用逻辑。我们可以把相关的响应式数据、计算属性和方法封装成一个组合式函数。比如我们可以把表单处理的逻辑提取出来// useForm.js import { ref } from vue export function useForm(initialValue) { const formData ref(initialValue) const errors ref({}) const validate () { // 验证逻辑... } const submit async () { if (validate()) { await api.submit(formData.value) } } return { formData, errors, submit } }然后在组件中使用import { useForm } from ./useForm setup() { const { formData, errors, submit } useForm({ name: , email: }) return { formData, errors, submit } }这种方式让我们的代码像乐高积木一样可以自由组合大大提高了代码的复用性。4. 实战构建待办事项应用4.1 项目结构与初始化让我们用setup函数构建一个完整的待办事项应用。首先创建项目结构todo-app/ ├── src/ │ ├── components/ │ │ ├── TodoList.vue │ │ └── TodoForm.vue │ ├── composables/ │ │ └── useTodos.js │ ├── App.vue │ └── main.js在main.js中初始化Vue应用import { createApp } from vue import App from ./App.vue createApp(App).mount(#app)4.2 核心功能实现首先在useTodos.js中创建待办事项的核心逻辑import { ref, computed } from vue export function useTodos() { const todos ref([]) const filter ref(all) // 添加待办事项 const addTodo (text) { todos.value.push({ id: Date.now(), text, completed: false }) } // 切换完成状态 const toggleTodo (id) { const todo todos.value.find(t t.id id) if (todo) { todo.completed !todo.completed } } // 过滤后的待办事项 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 } }) return { todos, filter, addTodo, toggleTodo, filteredTodos } }然后在App.vue中使用这个组合式函数template div classtodo-app h1待办事项/h1 TodoForm addaddTodo / TodoList :todosfilteredTodos toggletoggleTodo / div classfilters button clickfilter all全部/button button clickfilter active未完成/button button clickfilter completed已完成/button /div /div /template script import { useTodos } from ./composables/useTodos import TodoForm from ./components/TodoForm.vue import TodoList from ./components/TodoList.vue export default { components: { TodoForm, TodoList }, setup() { const { todos, filter, addTodo, toggleTodo, filteredTodos } useTodos() return { filter, addTodo, toggleTodo, filteredTodos } } } /script4.3 组件实现TodoForm.vue负责添加新待办事项template form submit.preventhandleSubmit input v-modelnewTodo placeholder输入待办事项... / button typesubmit添加/button /form /template script import { ref } from vue export default { emits: [add], setup(props, { emit }) { const newTodo ref() const handleSubmit () { if (newTodo.value.trim()) { emit(add, newTodo.value) newTodo.value } } return { newTodo, handleSubmit } } } /scriptTodoList.vue显示待办事项列表template ul li v-fortodo in todos :keytodo.id click$emit(toggle, todo.id) :class{ completed: todo.completed } {{ todo.text }} /li /ul /template script export default { props: [todos], emits: [toggle] } /script5. 常见问题与最佳实践5.1 避免的陷阱在使用setup函数时有几个常见的坑需要注意不要混合使用Options API和Composition API虽然技术上可行但会让代码难以维护。选择一种风格并坚持使用。异步setup的问题setup函数默认应该是同步的。如果需要异步操作可以在setup内部使用async/await但不能把setup本身声明为async函数。响应式丢失问题解构props或reactive对象时要小心必要时使用toRefs保持响应性。5.2 性能优化技巧合理使用computed对于需要复杂计算的派生数据使用computed可以缓存结果避免不必要的重复计算。使用shallowRef和shallowReactive当你知道某些数据不需要深度响应时可以使用这些浅层响应式API来提升性能。合理组织代码把不相关的逻辑拆分到不同的组合式函数中可以减少组件的重新渲染。5.3 测试策略测试setup-based组件其实更简单了因为逻辑都被封装在独立的函数中。我们可以直接测试组合式函数import { useTodos } from ./useTodos import { ref } from vue test(addTodo should add new todo, () { const todos ref([]) const { addTodo } useTodos(todos) addTodo(测试待办项) expect(todos.value.length).toBe(1) expect(todos.value[0].text).toBe(测试待办项) })对于组件测试Vue Test Utils提供了专门的方法来测试setup-based组件import { mount } from vue/test-utils import TodoForm from ./TodoForm.vue test(emits add event with new todo, async () { const wrapper mount(TodoForm) await wrapper.find(input).setValue(新任务) await wrapper.find(form).trigger(submit) expect(wrapper.emitted().add[0]).toEqual([新任务]) })