Vue 3 组件定义的五种主流方式:从 Options API 到 Composition API 的演进与实战选型

发布时间:2026/8/24 22:41:03
Vue 3 组件定义的五种主流方式:从 Options API 到 Composition API 的演进与实战选型 1. 从“一个组件”到“五种写法”Vue 3 组件定义的演进与选择最近在带团队做项目评审发现一个挺有意思的现象同样一个功能按钮组里五个前端同学写出来的组件定义方式居然都不一样。有老派坚持用defineComponent的有尝鲜用setup语法糖的还有直接用ref和函数式写法的。这让我意识到Vue 3 的 Composition API 带来的不仅仅是 API 的变化更是一种组件编写范式的解放。它不再像 Vue 2 时代那样几乎只有export default { ... }这一种标准答案。现在面对不同的场景我们有了更多、更灵活的选择。这篇文章我就结合自己这几年从 Vue 2 迁移到 Vue 3以及在多个中大型项目中实践的经验来系统性地聊聊在 Vue 3 中定义组件的 5 种主流方式。这不仅仅是“怎么写”的问题更是“为什么这么写”以及“在什么场景下用哪种写法更合适”的思考。无论你是刚刚接触 Vue 3正在为面试题“Vue 3 组件定义有哪些方式”而头疼的新手还是已经上手但想更深入理解不同写法优劣的进阶开发者相信都能从中找到一些实用的参考和启发。我们会从最经典的写法开始逐步深入到更现代、更简洁的范式并探讨它们各自的最佳实践和那些容易踩的“坑”。2. 基石Options API 与defineComponent的经典延续即便在 Composition API 大行其道的今天Options API 依然是 Vue 3 的完全合法公民并且通过defineComponent这个全局 API 得到了更好的类型支持。这是许多从 Vue 2 迁移过来的项目最平滑的过渡路径也是许多官方文档示例的起点。2.1defineComponent的核心价值类型安全与工具支持在 Vue 2 中我们直接导出一个对象export default { ... }。在 Vue 3 中虽然你依然可以这么做但强烈推荐使用defineComponent来包裹这个对象。它的核心价值不在于运行时而在于开发时提供完善的 TypeScript 类型推断。当你使用defineComponent定义组件时Vue 的类型系统能够智能地推断出data、computed、methods以及props的类型。这意味着你在模板中使用{{ message }}或者在方法中调用this.someMethod()时你的 IDE如 VSCode 配合 Volar 插件能够提供准确的自动补全和类型检查极大减少了因拼写错误或类型不匹配导致的运行时 Bug。import { defineComponent } from vue; export default defineComponent({ name: ClassicButton, props: { label: { type: String, required: true }, disabled: { type: Boolean, default: false } }, data() { return { clickCount: 0 }; }, computed: { buttonClasses(): string { return this.disabled ? btn disabled : btn primary; } }, methods: { handleClick() { if (!this.disabled) { this.clickCount; this.$emit(click, this.clickCount); } } }, mounted() { console.log(组件已挂载初始点击次数, this.clickCount); } });在上面的例子中this.label、this.disabled、this.clickCount的类型都是明确的。如果你在handleClick方法里错误地写了this.clickcount小写cTypeScript 或 Volar 会立刻报错提示属性不存在。注意即使你的项目不使用 TypeScript使用defineComponent也是一个好习惯。因为 Volar 等工具同样能为纯 JavaScript 项目提供基于 JSDoc 的智能提示提升开发体验。2.2 Options API 的适用场景与局限性这种方式最适合哪些场景呢Vue 2 升级项目对于存量 Vue 2 项目逐步迁移到 Vue 3 时可以保持组件写法基本不变只需将Vue.extend或直接导出的对象改为defineComponent包裹迁移成本最低。逻辑简单的小型组件当组件逻辑非常简单只有几个数据属性和一两个方法时Options API 的结构清晰直观所有东西都分门别类地放在data、methods等选项里一目了然。对this上下文有强依赖的代码Options API 中data、computed、methods、生命周期钩子都共享同一个this上下文访问属性非常直接。然而它的局限性在复杂组件中会变得非常明显逻辑关注点分离困难一个“用户资料卡片”组件可能同时包含“获取用户数据”、“格式化显示”、“交互逻辑”等多个功能。在 Options API 中这些逻辑的代码会被拆分到data、methods、computed、watch、mounted等多个选项中导致阅读和维护时需要不断上下滚动文件这就是所谓的“逻辑关注点碎片化”。代码复用性差相似的逻辑比如一个拖拽功能很难在多个组件之间优雅地抽取和复用。虽然可以用 Mixin但 Mixin 会带来命名冲突、来源不清晰等问题。类型系统在复杂场景下略显笨拙尤其是在处理动态添加的属性或复杂的计算属性链时类型推断有时会力不从心。实操心得在全新的 Vue 3 项目中我通常只会在两种情况下使用纯 Options API一是快速原型验证写个 demo二是封装一些极其简单、无状态的展示型组件。对于任何包含业务逻辑的组件我都会倾向于使用接下来要介绍的 Composition API 写法。3. 核心范式Composition API 与setup()函数Composition API 是 Vue 3 的革命性特性它解决了 Options API 在逻辑复杂度和复用性上的痛点。其核心是setup()函数它是组件的“逻辑入口”。3.1setup()函数的工作机制setup()函数在组件实例创建之前执行它接收两个参数props一个响应式对象包含了组件接收的所有 prop。context一个普通对象暴露了三个组件属性attrs非 prop 的 attribute、slots插槽和emit触发事件的方法。setup()的返回值是一个对象这个对象的所有属性都会被暴露给组件的模板template和其他 Options API 选项如computed、watch等如果混用的话。import { defineComponent, ref, computed, onMounted } from vue; export default defineComponent({ name: CompositionButton, props: { label: String, disabled: Boolean }, emits: [click], // 显式声明事件利于类型检查和文档化 setup(props, context) { // 1. 响应式状态 const clickCount ref(0); // 2. 计算属性 const buttonClasses computed(() { return props.disabled ? btn disabled : btn primary; }); // 3. 方法 const handleClick () { if (!props.disabled) { clickCount.value; context.emit(click, clickCount.value); } }; // 4. 生命周期钩子 onMounted(() { console.log(组件已挂载初始点击次数, clickCount.value); }); // 5. 返回给模板使用的所有内容 return { clickCount, buttonClasses, handleClick }; } });注意几个关键变化没有this在setup()中无法访问this所有组件实例的上下文都通过参数 (props,context) 或生命周期钩子获得。响应式数据使用ref或reactive创建。访问 Props直接通过props参数访问且是响应式的。但不要解构props否则会失去响应性。如果需要解构应使用toRefs。触发事件通过context.emit。生命周期通过形如onMounted、onUpdated的导入函数来注册。3.2 为什么选择 Composition API逻辑复用的魅力Composition API 最大的优势在于逻辑关注点的聚合和超强的可复用性。假设我们有一个“鼠标位置跟踪”的逻辑在 Options API 中你需要在data里定义x,y在methods里定义updateMouse在mounted和beforeUnmount里添加和移除事件监听器。代码是分散的。而在 Composition API 中我们可以将其抽取成一个组合式函数 (Composable)// useMouse.js import { ref, onMounted, onBeforeUnmount } from vue; export function useMouse() { const x ref(0); const y ref(0); const updateMouse (event) { x.value event.pageX; y.value event.pageY; }; onMounted(() window.addEventListener(mousemove, updateMouse)); onBeforeUnmount(() window.removeEventListener(mousemove, updateMouse)); return { x, y }; }然后在任何组件中你都可以像使用函数一样使用它// MyComponent.vue import { useMouse } from ./useMouse; export default defineComponent({ setup() { const { x, y } useMouse(); // ... 其他逻辑 return { x, y }; } });这样与“鼠标跟踪”相关的所有代码状态、方法、生命周期都聚集在一个可复用的函数里清晰且易于维护。这就是 Composition API 的核心理念基于逻辑功能来组织代码而不是基于选项类型。踩坑提醒在setup()中使用async/await需要格外小心。因为setup必须同步返回一个对象。如果你需要异步获取数据常见的模式是在setup内部调用异步函数但返回一个同步的响应式状态。// 错误setup 返回了一个 Promise async setup() { const data await fetchData(); return { data }; // 返回 Promise{ data } } // 正确在 setup 内部处理异步但返回同步状态 setup() { const data ref(null); const error ref(null); fetchData() .then(res data.value res) .catch(err error.value err); return { data, error }; }对于更复杂的异步场景可以考虑使用Suspense组件实验性或第三方状态管理库。4. 语法糖的胜利script setup与响应式编译宏如果你觉得setup()函数的写法还是有点啰嗦需要返回对象访问 props 和 emit 不够直接那么script setup语法糖就是为你准备的。这是目前 Vue 3 单文件组件 (SFC) 中最流行、最推荐的写法它通过编译时的魔法极大地简化了 Composition API 的使用。4.1 极简主义告别return的模板代码在script setup中任何顶层的绑定变量、函数、import 引入都会自动暴露给模板无需再手动return。script setup langts import { ref, computed, onMounted } from vue; // 1. Props 和 Emits 使用编译器宏定义 const props defineProps{ label: string; disabled?: boolean; }(); const emit defineEmits{ (e: click, count: number): void; }(); // 2. 响应式状态与计算属性 const clickCount ref(0); const buttonClasses computed(() { return props.disabled ? btn disabled : btn primary; }); // 3. 方法 const handleClick () { if (!props.disabled) { clickCount.value; emit(click, clickCount.value); } }; // 4. 生命周期 onMounted(() { console.log(组件已挂载); }); // 5. 导入的辅助函数或组件也可直接用于模板 import { formatCurrency } from /utils; import ChildComp from ./ChildComp.vue; /script template button :classbuttonClasses clickhandleClick {{ label }} (Clicked: {{ clickCount }}) /button ChildComp / div{{ formatCurrency(100) }}/div /template可以看到代码变得异常简洁。defineProps和defineEmits是编译器宏它们在编译阶段会被处理掉不会出现在运行时代码中。它们提供了最直接的类型安全 Prop/Emit 定义方式尤其配合 TypeScript。4.2 编译器宏详解与高级用法script setup提供了几个核心的编译器宏defineProps/defineEmits用于定义组件的输入和输出支持运行时声明和基于类型的声明两种方式。上例使用的是基于类型的声明这是最简洁的类型安全写法。defineExpose默认情况下script setup中的组件是“封闭”的父组件无法通过模板ref访问其内部属性。如果你需要暴露某些属性或方法需要使用defineExpose。script setup import { ref } from vue; const internalState ref(secret); const publicMethod () { console.log(called from parent); }; defineExpose({ publicMethod, someValue: internalState // 可以暴露 ref 本身 }); /scriptuseSlots/useAttrs在模板中可以直接使用$slots和$attrs但在script setup的 JS 逻辑中如果需要访问它们需要使用这两个工具函数。script setup import { useSlots, useAttrs } from vue; const slots useSlots(); const attrs useAttrs(); console.log(slots.default); // 检查是否有默认插槽内容 console.log(attrs.class); // 获取外部传入的 class /script性能与心智模型优势script setup不仅仅是语法糖。由于编译时的优化它通常能生成比普通setup()函数更高效的运行时代码。更重要的是它让开发者的心智模型更简单顶层的代码就是组件的逻辑无需再理解setup函数的返回机制。一个常见的坑由于script setup中顶层的await会导致该组件被自动包装为一个异步依赖因此其父组件需要使用Suspense来包裹。如果你不希望这样或者在不支持Suspense的环境如 Vue Router 的导航守卫请将异步逻辑放在一个非顶层的作用域中例如在一个函数内部。script setup // 这会使得该组件成为异步组件 const data await fetchData(); // 更好的做法将异步逻辑封装 const data ref(null); onMounted(async () { data.value await fetchData(); }); /script5. 函数式组件与渲染函数追求极致控制当模板的声明式语法无法满足极其动态或复杂的渲染需求时我们可以退回到更底层的 JavaScript/TypeScript使用渲染函数 (Render Function)或函数式组件 (Functional Component)来定义组件。这在开发高级组件库或处理高度动态的 UI 时非常有用。5.1 使用h()与渲染函数Vue 提供了一个h()函数代表“hyperscript”用于创建虚拟 DOM 节点。你可以直接编写一个返回 VNode 树的setup函数。import { defineComponent, h, ref } from vue; export default defineComponent({ name: DynamicHeading, props: { level: { type: Number, required: true, validator: (val) val 1 val 6 }, text: String }, setup(props) { // 渲染函数可以访问响应式状态和 props const isHovered ref(false); return () { // h() 函数接收三个参数标签名、属性对象、子节点 return h( h${props.level}, // 动态标签名如 h1, h2 { class: [dynamic-heading, { is-hovered: isHovered.value }], onMouseenter: () (isHovered.value true), onMouseleave: () (isHovered.value false), id: heading-${props.level} }, [ props.text || Default Title, // 可以嵌套更多的 h() 调用 isHovered.value ? h(small, (Hovering!)) : null ] ); }; } });这种方式给了你完全的编程式控制能力。你可以根据props或状态动态决定渲染什么标签、什么属性、什么子元素。在一些需要根据复杂数据生成大量相似节点如虚拟列表的场景下渲染函数可能比模板更高效因为它避免了模板编译的开销。5.2 无状态函数式组件在 Vue 3 中函数式组件的概念有所变化。它特指一个没有自身响应式状态、没有实例生命周期的组件它只接收props和context包含slots,attrs,emit并返回渲染内容。它被编译为一个接收props并返回 VNode 的函数性能开销极低。import { h } from vue; // 定义一个函数式组件 const FunctionalButton (props, context) { const { label, disabled false } props; const { emit } context; return h( button, { class: disabled ? btn disabled : btn, disabled: disabled, onClick: () !disabled emit(click) }, label ); }; // 为函数式组件添加 props 定义为了类型提示和运行时检查 FunctionalButton.props [label, disabled]; FunctionalButton.emits [click]; export default FunctionalButton;或者使用defineComponent并指定functional: trueOptions API 风格import { defineComponent } from vue; export default defineComponent({ name: FunctionalButton, functional: true, // 标记为函数式组件 props: [label, disabled], emits: [click], render(props, context) { const { emit } context; return h(button, { class: props.disabled ? btn disabled : btn, disabled: props.disabled, onClick: () !props.disabled emit(click) }, props.label); } });使用场景与权衡函数式组件非常适合纯展示型的、无状态的简单组件比如一个只根据 props 渲染一段文本或图标的组件。由于没有组件实例它的挂载和更新非常快。但是你也就无法使用ref、computed、生命周期钩子等特性。对于绝大多数业务组件script setup是更通用、更强大的选择。渲染函数和函数式组件更像是你的“秘密武器”在特定场景下解决特定问题。经验之谈不要为了“炫技”而滥用渲染函数。模板在绝大多数情况下更具可读性和可维护性。只有当模板的静态结构成为瓶颈例如需要根据一个庞大的配置对象动态生成数百种不同的表单字段布局或者你在开发一个需要极致性能的基础组件库时才考虑使用渲染函数。在业务开发中我几乎 99% 的时间都在使用script setup。6. 拥抱 JSX/TSX另一种表达方式虽然 Vue 的模板语法非常强大且易于上手但对于来自 React 背景或更喜欢 JavaScript 原生表达力的开发者Vue 3 也提供了对 JSX或 TSX的官方支持。你可以直接在.vue文件的script标签中或者在.jsx/.tsx文件中使用 JSX 语法。6.1 在 Vue 3 中配置与使用 JSX首先你需要确保项目支持 JSX。对于 Vite 项目这通常意味着安装了vitejs/plugin-vue-jsx。npm install vitejs/plugin-vue-jsx然后在vite.config.js中配置import vue from vitejs/plugin-vue; import vueJsx from vitejs/plugin-vue-jsx; export default { plugins: [ vue(), vueJsx(), // 启用 JSX 支持 ], };配置好后你就可以编写 JSX 组件了// Button.tsx import { defineComponent, ref } from vue; export default defineComponent({ name: JsxButton, props: { label: String, disabled: Boolean }, emits: [click], setup(props, { emit }) { const clickCount ref(0); const handleClick () { if (!props.disabled) { clickCount.value; emit(click, clickCount.value); } }; return () ( button class{props.disabled ? btn disabled : btn primary} disabled{props.disabled} onClick{handleClick} {props.label} (Clicked: {clickCount.value}) /button ); } });可以看到setup函数返回了一个渲染函数而这个渲染函数直接使用了 JSX 语法。JSX 让你能够利用 JavaScript 的全部能力来构建 UI比如在{}内可以执行任何 JavaScript 表达式可以方便地使用map、filter来渲染列表可以使用条件运算符进行条件渲染。6.2 JSX 与模板的抉择何时用怎么选JSX 和 Vue 模板各有优劣选择哪一种更多是团队偏好和项目背景的考量。JSX 的优势JavaScript 原生对于熟悉 JavaScript 的开发者来说学习成本更低。所有的逻辑条件、循环都用 JavaScript 语法表达上下文切换少。类型安全在 TypeScript 项目中JSX 能提供更出色的类型检查和编辑器支持模板中的表达式类型推断有时会受限。灵活性可以非常方便地使用高阶组件、渲染属性等模式这些在纯模板中实现起来比较别扭。代码复用可以轻松地将 JSX 片段提取为 JavaScript 函数实现更细粒度的复用。模板的优势声明式与直观模板更贴近最终的 HTML 结构对于设计师或后端开发者来说更易读。v-if、v-for等指令的意图非常清晰。优化潜力Vue 的模板编译器能够进行静态分析并做出更多的编译时优化如静态节点提升这在某些情况下可能带来微小的性能优势。工具链丰富有专门的模板语法高亮、格式化如 Prettier 插件、IDE 插件Volar支持体验非常完善。社区主流Vue 生态中绝大多数库、示例和最佳实践都是基于模板的。我的建议如果你的团队有丰富的 React 经验或者项目对 TypeScript 类型安全有极高要求且 UI 逻辑极其复杂、动态性强那么 JSX 是一个很好的选择。对于大多数典型的 Vue 项目尤其是偏重展示和表单交互的中后台管理系统模板语法因其简洁和直观依然是更主流、更高效的选择。我们团队在开发基础组件库时部分高度动态、逻辑复杂的组件会采用 JSX而业务组件几乎全部使用script setup配合模板。7. 实战选型指南五种方式的应用场景与决策矩阵了解了五种定义方式后在实际项目中如何选择这并非非此即彼而是要根据组件类型、项目阶段和团队习惯来权衡。下面我结合自己的经验给出一个决策参考。定义方式核心特点最佳适用场景需要谨慎或避免的场景Options API defineComponent经典、结构固定、this上下文、类型支持好1. Vue 2 向 Vue 3 的迁移期项目。2. 逻辑极其简单的纯展示型组件。3. 团队成员对 Options API 有深厚感情和习惯。逻辑复杂的业务组件易导致代码分散。需要高度逻辑复用的场景。Composition API (setup())逻辑聚合、高复用性、无this、灵活1. 逻辑复杂的业务组件需要将相关代码组织在一起。2. 需要抽取组合式函数 (Composables) 在多个组件间复用逻辑。3. 作为理解 Composition API 原理的过渡学习。极其简单的组件可能杀鸡用牛刀。对setup函数返回值机制不熟悉时。script setup语法糖极简、现代、开发体验最佳、编译时优化当前 Vue 3 SFC 的默认推荐写法。适用于绝大多数新组件尤其是业务组件和通用组件。需要暴露大量内部方法给模板 ref 的场景需配合defineExpose。需要兼容不支持此语法糖的古老构建工具现已极少见。渲染函数 /h()完全编程式控制、动态性极强、性能潜力1. 开发高级组件库需要渲染高度动态或复杂的结构。2. 需要根据运行时数据生成完全不同标签树的场景。3. 对渲染性能有极致要求的特定组件如超大型虚拟列表。一般的业务开发。可读性和可维护性要求高的场景。JSX/TSXJavaScript 原生表达力、类型安全好、灵活1. 团队有 React 背景希望统一技术栈心智模型。2. 组件逻辑极度复杂且与 TypeScript 深度集成需要最强类型支持。3. 偏好用 JavaScript 完整控制渲染逻辑。团队对 Vue 模板语法更熟悉。项目依赖大量基于模板的第三方库。希望获得 Vue 模板编译器的特定优化。一个项目的混合使用策略 在一个真实的项目中你很可能同时使用多种方式。例如主体90% 的业务组件使用script setup享受其简洁和高效。工具将可复用的逻辑如useMouseuseFetch抽成 Composition API 编写的组合式函数。底层组件库中的某些高级组件如动态表单渲染器、树形控件可能使用渲染函数来实现其核心的动态渲染逻辑。兼容从旧项目迁移过来的个别复杂组件可能暂时保留 Options API 写法逐步重构。最后的建议对于 Vue 3 的新项目我的首要推荐是script setup。它代表了 Vue 3 组合式 API 最先进、最优雅的用法。从它开始学习你能最快地拥抱 Vue 3 的核心优势。当你在使用过程中遇到其边界情况时再回过头来了解setup()函数、渲染函数等其他方式你会对 Vue 3 的整个组件系统有更深刻的理解。记住没有“最好”的写法只有“最适合”当前场景和团队的写法。理解每种方式背后的理念和代价才能做出明智的选择。

相关新闻