ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue 3 Option API与Composition API深度对比:从TodoList项目看开发范式演进

Vue 3 Option API与Composition API深度对比:从TodoList项目看开发范式演进 1. 项目概述从Option到Composition一次Vue开发范式的深度实践最近在社区里看到不少关于Vue 3两种API风格——Option API和Composition API的讨论很多刚上手的朋友会疑惑到底该学哪个用哪个它们在实际项目中区别真有那么大吗正好我手头有一个经典到不能再经典的练手项目TodoList待办事项列表。这次我们不只满足于“实现功能”而是要分别用Option API和Composition API完整地撸一遍从零搭建深入对比两者在代码组织、逻辑复用、响应式处理乃至开发心流上的差异。这不仅仅是一个功能实现更像是一次Vue核心编程思想的沉浸式体验。无论你是Vue 2的老兵正在向Vue 3迁移还是刚入门Vue 3的新手想理清脉络相信这个对比实践都能给你带来不少启发。我们会从最基础的列表展示、添加、删除、切换状态开始逐步加入筛选、持久化等常见功能在实现相同业务逻辑的过程中切身感受两种范式带来的不同开发体验。2. 核心思路与架构设计解析在动手写代码之前我们先花点时间厘清这个TodoList的核心需求与设计思路。一个基础的TodoList通常包含以下功能点1) 展示待办事项列表2) 新增待办事项3) 标记事项为完成/未完成4) 删除事项5) 根据状态全部/进行中/已完成筛选列表。此外为了提升体验我们可能还会加入本地存储防止页面刷新后数据丢失。基于这些功能我们的状态数据设计就清晰了需要一个数组来存储所有的待办事项每个事项是一个对象至少包含id唯一标识、text内容、done完成状态这几个字段。我们还需要一个变量来存储当前的筛选状态。那么Option API和Composition API将如何承载这些状态和逻辑呢这就是本次对比的核心。Option API的设计思路是“选项式”的。我们会创建一个Vue组件然后在data()选项中定义todos数组和filter状态在methods选项中定义addTodo、removeTodo、toggleTodo等方法在computed选项中定义过滤后的列表filteredTodos在watch或created/mounted生命周期中处理本地存储的读写。所有的逻辑按照选项的类型被分门别类地放置结构清晰尤其是对于熟悉Vue 2或面向对象编程的开发者来说这种“按职责分块”的方式非常直观。Composition API的设计思路则是“组合式”的。它不再受限于固定的选项而是允许我们在setup()函数或script setup语法糖中自由地组织代码。我们的思路会转变为首先使用ref或reactive创建响应式状态todos,filter然后围绕这些状态编写一系列处理函数addTodo,removeTodo等接着使用computed创建计算属性最后可能还会用到watch或onMounted等生命周期钩子。关键点在于我们可以把相关联的状态和逻辑“组合”在一起而不是分散在不同的选项里。例如所有与“待办事项CRUD”相关的状态和函数可以放在一个逻辑块内所有与“筛选”相关的放在另一个逻辑块。这种基于逻辑关注点而非选项类型的代码组织方式是Composition API的精髓尤其在处理复杂组件或逻辑复用时有巨大优势。3. 基于Option API的实现与细节剖析让我们先来看看如何使用经典的Option API来构建这个TodoList。我会假设你已经有了一定的Vue基础所以会重点讲解实现细节和容易踩坑的地方。3.1 状态定义与初始化在data()函数中我们返回一个对象其中包含组件的所有响应式状态。这里有两个核心状态todos列表和当前的filter类型。export default { name: TodoListOption, data() { return { // 待办事项列表初始从本地存储读取 todos: JSON.parse(localStorage.getItem(my-todos)) || [], // 当前筛选类型all(全部), active(进行中), completed(已完成) filter: all } } }这里有一个实操心得直接从localStorage读取数据并赋值给todos是安全的因为JSON.parse在遇到null时会返回null而||操作符会将其转换为空数组[]。这是一种简洁的默认值处理方式。但请注意如果本地存储的数据格式不是合法的JSON字符串JSON.parse会抛出错误。在生产环境中你可能需要更健壮的错误处理例如使用try...catch。3.2 方法Methods的实现接下来在methods选项中我们实现所有操作数据的方法。methods: { addTodo(newTodoText) { if (!newTodoText.trim()) return // 防止输入空内容 const newTodo { id: Date.now(), // 使用时间戳作为简单ID text: newTodoText.trim(), done: false } this.todos.unshift(newTodo) // 新增的放在最前面 }, removeTodo(id) { const index this.todos.findIndex(todo todo.id id) if (index -1) { this.todos.splice(index, 1) } }, toggleTodo(id) { const todo this.todos.find(todo todo.id id) if (todo) { todo.done !todo.done } }, clearCompleted() { this.todos this.todos.filter(todo !todo.done) } }注意事项直接修改数组与响应式更新this.todos.unshift()、this.todos.splice()这些方法能直接修改原数组并触发视图更新因为Vue对数组的变更方法如push,pop,shift,unshift,splice,sort,reverse进行了包裹。这是完全可行的。查找与修改对象在toggleTodo方法中我们通过find找到了数组中的对象然后直接修改了它的done属性。由于这个对象本身就是响应式的所以修改其属性也能触发更新。这里的关键是我们修改的是已存在于响应式数组中的对象的属性。ID生成这里用Date.now()生成ID在简单场景下够用但在极快速连续操作时可能有极小概率重复。对于正式项目建议使用更可靠的库如uuid。3.3 计算属性Computed与列表筛选筛选功能是TodoList的亮点我们通过计算属性filteredTodos来实现。computed: { filteredTodos() { switch (this.filter) { case active: return this.todos.filter(todo !todo.done) case completed: return this.todos.filter(todo todo.done) default: // all return this.todos } }, // 辅助计算属性统计未完成项数量 activeTodoCount() { return this.todos.filter(todo !todo.done).length }, // 辅助计算属性判断是否显示“清除已完成”按钮 showClearCompleted() { return this.todos.some(todo todo.done) } }计算属性的妙处在于它是基于其依赖的响应式状态进行缓存的。只有当this.todos或this.filter发生变化时filteredTodos才会重新计算。在模板中多次使用filteredTodos也只会计算一次性能高效。3.4 生命周期与本地持久化为了在页面刷新后不丢失数据我们需要在数据变化时将其保存到localStorage并在组件初始化时读取。watch: { // 深度监听todos数组的变化 todos: { handler(newTodos) { localStorage.setItem(my-todos, JSON.stringify(newTodos)) }, deep: true // 必须深度监听因为我们在修改todo对象的属性 } }, // 或者使用 mounted 钩子确保初始读取data中已做此为备选 mounted() { // 如果data()中没有初始化可以在这里读取 // const saved localStorage.getItem(my-todos) // if (saved) { // try { // this.todos JSON.parse(saved) // } catch(e) { // console.error(解析本地存储数据失败:, e) // } // } }重要避坑点监听todos数组时必须设置deep: true。因为我们的toggleTodo方法是直接修改数组内对象的属性这种嵌套对象属性的修改默认的浅层监听是捕捉不到的。如果不加deep: true只有整个数组被替换如this.todos newArray或者使用数组的变更方法时监听器才会触发。3.5 模板与样式要点模板部分相对直观这里提几个关键点template div classtodo-app header h1待办事项 (Option API)/h1 input v-model.trimnewTodoText keyup.enteraddTodo(newTodoText); newTodoText placeholder需要做点什么 classnew-todo / /header section classmain v-iftodos.length ul classtodo-list li v-fortodo in filteredTodos :keytodo.id :class{ completed: todo.done } div classview input typecheckbox classtoggle :checkedtodo.done changetoggleTodo(todo.id) / label dblclickenterEditMode(todo){{ todo.text }}/label button classdestroy clickremoveTodo(todo.id)/button /div !-- 编辑输入框略 -- /li /ul /section footer classfooter v-iftodos.length span classtodo-count strong{{ activeTodoCount }}/strong 项待办 /span ul classfilters li v-fortype in [all, active, completed] :keytype a :href#/${type} :class{ selected: filter type } click.preventfilter type {{ { all: 全部, active: 进行中, completed: 已完成 }[type] }}/a /li /ul button classclear-completed v-ifshowClearCompleted clickclearCompleted 清除已完成 /button /footer /div /template样式与交互细节v-model.trim在输入框上使用.trim修饰符可以自动去除用户输入的首尾空格避免创建无意义的空白待办项。keyup.enter监听回车键事件是添加待办事项的经典交互。注意在事件处理中清空输入框。:key的重要性在v-for循环中为每个li绑定唯一的:keytodo.id是必须的。这能帮助Vue高效地跟踪每个节点的身份在列表更新时进行最小化的DOM操作避免出现奇怪的渲染bug。条件渲染使用v-iftodos.length来控制列表和底栏的显示当没有待办事项时提供一个更清爽的界面。至此一个功能完整的Option API版TodoList就完成了。它的结构非常规整所有数据在data里方法在methods里计算属性在computed里一目了然。对于这样一个中等复杂度的组件Option API表现得游刃有余。4. 基于Composition API的重构与逻辑组合现在让我们用Composition API来重新实现一遍完全相同的功能。我们将使用Vue 3的script setup语法糖这是目前最推荐、最简洁的写法。你会立刻感受到代码组织方式的根本性变化。4.1 响应式状态的定义在script setup中我们首先引入需要的API然后定义响应式状态。script setup import { ref, computed, watch, onMounted } from vue // 1. 响应式状态定义 const todos ref(JSON.parse(localStorage.getItem(my-todos)) || []) const filter ref(all) // all, active, completed const newTodoText ref() /script核心变化我们不再使用data()函数返回一个对象而是直接使用ref()或reactive()来创建响应式引用。ref用于定义基本类型如字符串、数字、布尔值或任何需要被替换的值的引用。在模板中直接使用在JS中需要通过.value访问。这里todos是一个数组我们也可以用ref因为后续我们可能会整体替换它如todos.value newArray。如果确定不会整体替换且想保持嵌套响应性使用reactive也可以const state reactive({ todos: [], filter: all })访问时用state.todos。我个人更喜欢对数组用ref因为它对类型推导更友好且.value的语义很清晰。4.2 逻辑函数的组合接下来我们把相关的操作封装成一个个独立的函数。这是Composition API“逻辑组合”思想的体现。// 2. 操作待办事项的逻辑 function addTodo(text) { if (!text.trim()) return todos.value.unshift({ id: Date.now(), text: text.trim(), done: false }) newTodoText.value // 清空输入框 } function removeTodo(id) { const index todos.value.findIndex(todo todo.id id) if (index -1) { todos.value.splice(index, 1) } } function toggleTodo(id) { const todo todos.value.find(todo todo.id id) if (todo) { todo.done !todo.done } } function clearCompleted() { todos.value todos.value.filter(todo !todo.done) }注意所有对todos的修改都需要通过.value来进行因为todos是一个ref对象。在模板中Vue会自动解包ref所以你可以直接写todos而不是todos.value。4.3 计算属性与侦听器计算属性和侦听器的定义也更加直接它们与相关的状态紧邻。// 3. 计算属性 const filteredTodos computed(() { switch (filter.value) { case active: return todos.value.filter(todo !todo.done) case completed: return todos.value.filter(todo todo.done) default: return todos.value } }) const activeTodoCount computed(() todos.value.filter(todo !todo.done).length) const showClearCompleted computed(() todos.value.some(todo todo.done)) // 4. 侦听器与副作用 watch(todos, (newTodos) { localStorage.setItem(my-todos, JSON.stringify(newTodos)) }, { deep: true }) // 如果需要组件挂载时执行操作可以使用生命周期钩子 // onMounted(() { // console.log(TodoList组件已挂载) // })与Option API的对比代码组织更自由你可以把addTodo、removeTodo函数和它们直接操作的todos状态放在一起定义形成一个逻辑块。这与Option API中必须把所有方法都丢进methods对象形成了鲜明对比。依赖关系更清晰在computed和watch中它们所依赖的响应式状态todos.value,filter.value直接作为函数参数或函数体内的引用依赖关系一目了然。而在Option API的computed或watch选项中你需要通过this.todos来访问依赖关系是隐式的。类型推导友好在TypeScript项目中Composition API的写法能获得更完美的类型推导因为变量和函数的类型是直接声明的。4.4 模板与逻辑的暴露在script setup中所有顶层声明的变量、函数都会自动暴露给模板无需return。模板部分与Option API版本几乎完全一致只需要将方法调用和属性访问的方式稍作调整实际上在模板中写法完全一样因为Vue自动处理了.value的解包。template !-- 模板结构与Option API版几乎完全相同 -- div classtodo-app input v-model.trimnewTodoText keyup.enteraddTodo(newTodoText) / ul li v-fortodo in filteredTodos :keytodo.id !-- ... -- button clickremoveTodo(todo.id)删除/button /li /ul button clickclearCompleted v-ifshowClearCompleted清除已完成/button /div /template一个重要的实践技巧对于更复杂的组件你可以利用Composition API将逻辑抽取到独立的**组合式函数Composables**中。例如我们可以创建一个useTodoList函数// useTodoList.js import { ref, computed, watch } from vue export function useTodoList(initialFilter all) { const todos ref(JSON.parse(localStorage.getItem(my-todos)) || []) const filter ref(initialFilter) const filteredTodos computed(() { // ... 筛选逻辑 }) function addTodo(text) { /* ... */ } function removeTodo(id) { /* ... */ } watch(todos, (newVal) { localStorage.setItem(my-todos, JSON.stringify(newVal)) }, { deep: true }) return { todos, filter, filteredTodos, addTodo, removeTodo // ... 返回其他需要暴露的状态和方法 } }然后在组件中直接使用script setup import { useTodoList } from ./useTodoList const { todos, filter, filteredTodos, addTodo, removeTodo } useTodoList() // 组件自身的其他逻辑... /script这种方式将TodoList的核心状态和逻辑完全封装、复用是Composition API在大型项目或组件库中体现巨大价值的场景。而在Option API中要实现类似的逻辑复用通常需要借助mixins但mixins存在命名冲突、数据来源不清晰等痛点。5. 深度对比与选型建议通过亲手实现两个版本我们对Option API和Composition API有了更感性的认识。下面我们从几个维度进行系统性的对比并给出选型建议。5.1 代码组织方式对比维度Option APIComposition API组织原则按选项类型组织data, methods, computed, watch, lifecycle。按逻辑关注点组织相关代码状态函数可以放在一起。阅读体验对于简单组件结构清晰一目了然。所有数据在data里找所有方法在methods里找。需要上下滚动阅读因为相关逻辑是纵向分布的。但逻辑块内部的内聚性更高。复杂组件当一个功能逻辑分散在data、methods、computed、watch中时阅读和调试需要不断跳转。可以将一个复杂功能的所有代码状态、计算、方法、副作用集中在一个逻辑块或组合式函数中内聚性极强。逻辑复用主要通过Mixins或作用域插槽实现容易发生命名冲突且数据来源模糊。通过**组合式函数Composables**实现类似React Hooks复用清晰灵活依赖关系明确。我的体会对于TodoList这种规模约100行逻辑的组件两者差异不大Option API甚至更规整。但当组件逻辑膨胀到数百行、多个功能交织时Composition API按功能切分代码块的优势就碾压性地体现出来了。你不再需要为了改一个功能而在data、methods、computed之间来回切换。5.2 响应式系统与类型支持维度Option APIComposition API响应式基础基于Vue 2的Object.definePropertyVue 3中Option API底层也用了Proxy。响应式数据定义在data()中。直接基于Vue 3的ref和reactiveProxy实现响应式数据是显式声明的变量。类型推导在TypeScript中支持一般this上下文下的类型推断有时需要额外配置。对TypeScript支持极佳变量和函数具有天然的类型组合式函数也能提供完美的类型导出。心智模型需要理解Vue特定的选项概念和this上下文。更接近标准的JavaScript/TypeScript使用普通的变量和函数心智负担更小。一个关键细节在Composition API中由于ref需要用.value访问初学者可能会忘记。但在模板和watch/computed的依赖自动收集中Vue会自动解包所以模板里写todos即可。而在组合式函数内部逻辑中必须记得写.value。这需要一个短暂的适应过程。5.3 学习曲线与团队协作维度Option APIComposition API学习门槛较低。概念少data, methods, computed...与许多后端MVC框架模式类似易于理解上手。较高。需要理解ref、reactive、computed、watch等新API以及script setup语法。对JavaScript闭包、函数式编程有一定要求。对新手友好度非常友好。提供了清晰的代码组织模板“填空”即可。初期可能感到困惑不知道代码该写在哪里、如何组织。但一旦掌握灵活性极高。团队协作强约束带来一致性。任何开发者写的Option API组件结构都差不多便于快速理解他人代码。灵活性高但也可能导致不同的开发者有不同的组织风格需要团队制定一定的规范如组合式函数的命名、组织方式。5.4 实战选型建议经过对比我的个人建议如下新手入门或小型项目从Option API开始。它概念清晰结构固定能帮你快速建立起Vue组件的基本心智模型把注意力集中在Vue的核心特性响应式、模板、指令上而不是纠结于代码组织。很多简单的后台管理系统、活动页面用Option API开发效率非常高。中大型项目或复杂组件强烈推荐使用Composition API。尤其是当你遇到需要抽取和复用业务逻辑、组件逻辑过于冗长难以维护时Composition API的组合式函数能力是救星。它让代码的可维护性和可测试性大大提升。Vue 2项目迁移如果现有Vue 2项目使用Option API且运行良好不必为了用而用Composition API重写。可以在新增的复杂组件或逻辑复用场景中逐步引入Composition API享受其带来的好处。Vue 3完全兼容Option API。新项目技术选型如果启动一个全新的Vue 3项目并且团队有一定JavaScript基础我倾向于以Composition API为主。script setup语法糖已经极大地简化了写法官方文档和生态也主要推荐Composition API。这是Vue未来的发展方向。最后的心得这两种API并非对立关系而是相辅相成。Option API是“声明式”的典范告诉你组件有什么Composition API是“命令式”组合的利器让你自由决定代码如何组织。理解两者的差异能让你更深刻地理解Vue的设计哲学。在实际项目中我甚至会根据组件的复杂程度混合使用Vue 3支持在同一个组件中同时使用两者但不推荐初学者这么做。掌握两者你就能在Vue的世界里更加游刃有余。
返回列表