
Vue3 进入组合式 API 时代之后setup函数就成了每个组件里绕不开的核心入口。很多刚接触 Vue3 的同学最大的困惑不是setup怎么定义而是setup的返回值到底能写什么、不能写什么。返回值写错了模板拿不到数据、事件触发不了、父组件通信失败这些问题排查起来往往比想象中更隐蔽。这篇文章就把setup的返回值彻底拆开讲清楚返回值可以是一个对象、一个函数甚至可以返回render函数返回对象时模板怎么取数据返回函数时又有什么限制为什么有人说return {}是 Vue3 装饰器有人说直接写script setup更快。看完之后你会发现setup返回值其实并不难关键是要把“渲染上下文”“响应式代理”“解包规则”这几个概念对齐。1. 核心能力速览能力项说明适用版本Vue 3.x组合式 API 核心语法核心作用将组件内部的数据、方法、计算属性暴露给模板或渲染函数返回值类型对象、函数、render函数对象返回值模板可访问属性、方法、计算属性响应式数据自动解包函数返回值直接返回渲染函数需要手动调用h()创建虚拟节点常用组合ref、reactive、computed、watch、provide、inject、onMounted等高频注意点返回ref时模板自动解包reactive对象不会二次解包props不可直接赋值最佳入门方式先理解setup返回值再对比script setup语法糖适用场景Vue3 组件封装、中后台系统、组件库开发、面试核心考点这张表不是花架子。setup的返回值直接决定了组件能否正常渲染、模板能否读到数据、事件能否正常触发。文章后面所有内容都围绕这张表展开。2. 适用场景与使用边界2.1 适合谁正在从 Vue2 选项式 API 迁移到 Vue3 的开发者想彻底搞懂组合式 API 运行原理的进阶学习者需要封装可复用业务组件的团队参与 Vue3 项目维护经常看到setup(props, context)写法的人准备 Vue3 面试想系统整理“setup 返回值”相关知识点的求职者2.2 能解决什么问题setup返回值是在组件的“逻辑定义阶段”和“模板渲染阶段”之间建立连接。它能解决以下典型问题模板中取不到ref定义的数据原因可能是没有正确 return。模板中修改数据不触发视图更新原因可能是返回了普通对象而不是响应式数据。父组件调用子组件方法失败原因可能是子组件setup没有把方法暴露出去或者使用了script setup时必须用defineExpose。希望用render函数完全控制渲染结果却不清楚怎么在setup里返回。项目里混用setup写法与script setup写法导致组织样式不统一。2.3 不适合什么场景不要把setup当成“万能逻辑层”。如果出现以下情况说明思路有问题把所有业务逻辑全部塞进一个setup函数导致组件几千行应当用组合式函数拆分。在setup中频繁操作 DOM项目绝大多数需求都能通过模板声明式渲染完成。强行在setup返回值里暴露大量内部依赖对象增加组件耦合度。2.4 开发与调试边界setup是组件实例创建阶段最先执行的逻辑之一。要注意此时组件实例尚未完全创建不能使用this。这是 Vue2 选项式代码迁移到 Vue3 时最常见的错误。调试时可以用console.log观察参数但要确保日志输出的是响应式代理对象而不是原始对象否则容易出现“值变了视图没变”的误导判断。3. 环境准备与基础写法3.1 环境检查学习setup返回值不需要特别重的环境一个基于 Vite 的 Vue3 项目足够了。可以先确认本地环境node -v npm -v如果还没有项目可以按通用方式创建# 通用示例实际安装提示以当前脚手架版本为准 npm create vuelatest进入对话式引导后选择 Vue3 相关选项不需要勾选额外插件也能跑通。如果你用的是 Vue CLI同样可以创建一个 Vue3 项目核心语法不受影响。3.2 setup 的基本形态setup是组件选项之一定义顺序和生命周期无关但它在组件实例创建时最先执行。script import { ref } from vue export default { setup(props, context) { const count ref(0) function increment() { count.value } return { count, increment } } } /script template div p{{ count }}/p button clickincrement1/button /div /template这段代码是setup返回值最经典的形态返回一个对象对象里包含一个ref数据和一个方法。模板中count不需要写.value这是 Vue3 模板自动解包的结果。3.3 返回值暴露给谁setup返回值只对“当前组件模板”和“当前组件的渲染函数”可见。它不会自动暴露给父组件。父组件能不能接触子组件里的数据和方法取决于子组件采用哪种写法普通setup返回的对象在开发工具中可以观察到父组件可以通过模板ref访问到暴露的方法和属性。script setup默认所有绑定对外部关闭需要显式使用defineExpose才能被父组件访问。这个差异在后面的章节会专门提到。4. setup 返回值详解对象返回的完整使用规则4.1 返回 ref 数据ref类型返回值在模板中会自动解包这是最常见的写法。script import { ref } from vue export default { setup() { const title ref(Vue3 setup 返回值) const count ref(1) return { title, count } } } /script template h1{{ title }}/h1 p当前次数{{ count }}/p /template这里有两个细节值得注意模板中的count是解包后的值但setup内部操作数据仍然要写count.value。如果返回的是一个嵌套对象例如const obj reactive({ detail: ref(1) })模板中obj.detail不会自动解包内部嵌套的ref。这个规则容易踩坑建议遇到嵌套结构时先打印出来确认。4.2 返回 reactive 数据reactive适合对象类型的数据。返回reactive对象后模板直接访问属性即可。script import { reactive } from vue export default { setup() { const user reactive({ name: 张三, age: 18 }) function updateName() { user.name 李四 } return { user, updateName } } } /script template p姓名{{ user.name }}/p p年龄{{ user.age }}/p button clickupdateName改名字/button /template有人会问user是响应式代理对象模板里改user.name是否触发更新可以的。reactive返回的就是 Proxy 代理后的对象模板中直接用.name访问没问题赋值操作也会触发响应式更新。4.3 返回 computedcomputed是setup返回值里经常用到的一类。返回计算属性后模板中直接使用值不需要额外解包。script import { ref, computed } from vue export default { setup() { const price ref(10) const number ref(3) const total computed(() price.value * number.value) return { price, number, total } } } /script template p单价{{ price }}/p p数量{{ number }}/p p总价{{ total }}/p /templatecomputed返回的是一个ComputedRef对象和ref类似模板中自动解包。这里要注意的是在setup内部访问total.value才能拿到计算后的值模板里直接写total即可。4.4 返回普通函数函数是setup返回值里最常见的“方法暴露”方式。script export default { setup() { function logMessage(msg) { console.log(自定义日志, msg) } function handleClick() { logMessage(按钮点击) } return { handleClick } } } /script template button clickhandleClick点击/button /template模板中使用返回的函数this指向由 Vue 绑定内部不需要也不应该依赖this。在setup中正常使用闭包、模块函数、组合式函数返回的函数即可。4.5 返回普通数据对象的误区很多初学者会以为把普通对象直接 return 后模板里修改值也能更新视图。这是错的。script export default { setup() { // 错误示例普通对象不具备响应式能力 const fakeUser { name: 王五 } function changeName() { // 视图不会自动更新 fakeUser.name 赵六 } return { fakeUser, changeName } } } /script template p{{ fakeUser.name }}/p button clickchangeName改名/button /template点击按钮后fakeUser.name可能在内存中变了但视图不会更新。如果这个数据要参与页面渲染且需要响应式变化必须使用ref或reactive。4.6 返回值不是把所有东西都暴露有人会把props、context、内部服务实例、请求实例等全部返回出去模板里又用不到。这不推荐。返回值越多模板上下文越冗杂越难维护。更稳妥的做法是模板需要的才返回工具函数尽量拆分到utils目录或组合式函数中。5. setup 返回值与模板渲染绑定5.1 为什么模板中 ref 不需要 .value很多人第一次看到“模板中不用写 .value”会惊讶。这是 Vue3 对响应式对象做 Unwrap Ref 的结果。模板编译过程中Vue 会检测当前渲染上下文中的数据如果是ref类型模板渲染时就自动取.value。这意味着模板里写{{ count }}实际读取的逻辑等价于count.value。如果在模板里写{{ count.value }}反而可能渲染异常因为模板已经把count解包成值了再取.value就成了从基本类型上取属性。5.2 返回 reactive 对象的属性访问返回reactive对象后模板中可以用user.name也可以先在setup中解构出来再返回script import { reactive } from vue export default { setup() { const user reactive({ name: 张三, age: 18 }) return { name: user.name, age: user.age } } } /script但这种写法会丢失响应性因为解构出来的是原始值。更推荐返回整个reactive对象或者在解构时使用toRefs。script import { reactive, toRefs } from vue export default { setup() { const user reactive({ name: 张三, age: 18 }) return { ...toRefs(user) } } } /scripttoRefs会把 reactive 对象里的每个属性转成ref模板中依然可以直接访问name、age同时保持响应性。5.3 setup 返回值中的事件处理返回的函数可以直接绑定到模板事件上也可以接收参数和事件对象。script export default { setup() { function handleChange(event) { console.log(change 事件, event.target.value) } function handleParam(msg) { console.log(自定义参数, msg) } return { handleChange, handleParam } } } /script template input changehandleChange / button clickhandleParam(来自模板的参数)传参/button /template5.4 v-model 与 setup 返回值setup返回值中只要有响应式数据和对应更新方法就能实现v-model的效果。这里有一个常见误区直接给返回的ref赋一个新值然后在模板里通过v-modelcount绑定。在组合式 API 中v-model对ref是支持的。script import { ref } from vue export default { setup() { const keyword ref() return { keyword } } } /script template input v-modelkeyword placeholder请输入关键词 / p当前输入{{ keyword }}/p /template注意v-modelkeyword的操作实际上是在模板编译时转化为对keyword.value的读写。所以这里不需要手动写keyword.value。6. setup 返回值与 render 函数6.1 返回函数的基本规则setup除了返回对象也可以返回一个函数。这个函数会被当作渲染函数执行。script import { h, ref } from vue export default { setup() { const count ref(0) function increment() { count.value } return () { return h(div, [ h(p, count: ${count.value}), h(button, { onClick: increment }, 1) ]) } } } /script这段代码没有使用模板页面上会渲染出一个计数器和按钮。点击按钮后count变化渲染函数重新执行页面更新。6.2 返回函数的注意点返回函数后模板和render不能同时使用。如果组件中既写了template又让setup返回渲染函数最终以setup返回的渲染函数为准但最好避免这种混淆写法。渲染函数直接返回虚拟节点时需要手动引入h函数。渲染函数里访问响应式数据要写.value因为这里已经是 JavaScript 执行环境不是模板环境。如果项目主要是模板写法没必要为了“炫技”改成render函数。render函数适合对渲染流程有完全控制需求的场景比如实现自定义组件、动态组件、渲染第三方数据结构等。6.3 返回对象和返回函数的选择对比项返回对象返回渲染函数模板支持支持不支持可读性适合大多数业务组件适合库、高阶组件开发效率高低可维护性高中使用场景普通业务、表单、列表通用组件、虚拟滚动、动态节点实际开发中 99% 的组件都应该使用返回对象或script setup。返回渲染函数更多是组件库作者的日常工作。7. setup 返回值与父子组件通信7.1 props返回值的只读边界setup的第一个参数是props。props是组件外部传入的响应式数据但在setup内部不可直接赋值。script export default { props: { title: { type: String, default: } }, setup(props) { console.log(props.title) // 错误props 是只读的 props.title 新的标题 return {} } } /script正确做法是如果想让父组件传入的数据在子组件中“修改后显示”可以基于 props 创建局部 refscript import { ref, watch } from vue export default { props: { title: { type: String, default: } }, setup(props) { const localTitle ref(props.title) watch(() props.title, (val) { localTitle.value val }) return { localTitle } } } /script7.2 emit通过 context 暴露事件setup的第二个参数context里包含emit。emit函数不一定要在setup内部直接使用也可以把 emit 包装成方法后返回给模板。script export default { emits: [update:name], setup(props, { emit }) { function changeName() { emit(update:name, 新的名字) } return { changeName } } } /script template button clickchangeName更新名字/button /template父组件监听template Child update:namehandleNameChange / /template script export default { methods: { handleNameChange(val) { console.log(子组件传来的, val) } } } /script这里的关键点setup返回值不包含emit但返回的方法闭包中已经捕获了context.emit所以模板中点击按钮可以正常触发父组件的监听。7.3 attrs 与 slotscontext.attrs包含非 props 属性比如class、id、自定义属性等。context.slots包含插槽内容。这两个东西也可以经过处理后返回给模板但更常见的做法是在setup中执行逻辑模板里直接用$attrs、$slots访问。script export default { setup(props, { attrs, slots }) { console.log(attrs:, attrs) console.log(slots:, slots) return {} } } /script7.4 子组件方法暴露给父组件普通setup返回对象里的方法父组件通过模板 ref 可以访问到。例如template Child refchildRef / button clickcallChildMethod调用子组件方法/button /template script import { ref } from vue export default { setup() { const childRef ref(null) function callChildMethod() { childRef.value.sayHello() } return { childRef, callChildMethod } } } /script子组件template div子组件/div /template script export default { setup() { function sayHello() { console.log(hello from child) } return { sayHello } } } /script父组件拿到childRef.value后就能访问子组件setup返回值中的sayHello方法。8. script setup 语法糖与 setup 返回值的关系8.1 本质是语法糖script setup是普通setup的编译期语法糖。编译器会把script setup中的顶层绑定自动暴露给模板省去手写return的步骤。script setup import { ref } from vue const count ref(0) function increment() { count.value } /script template p{{ count }}/p button clickincrement1/button /template这段代码等价于普通setup返回{ count, increment }的写法。模板中直接使用顶层变量即可。8.2 需要显式暴露给父组件时用 defineExposescript setup默认不会把顶层绑定作为组件实例属性暴露给父组件。父组件通过模板 ref 访问不到子组件中的 count 或 increment。需要暴露时script setup import { ref } from vue const count ref(0) function increment() { count.value } defineExpose({ count, increment }) /script对应普通setup写法的含义就是返回值中只有这里定义的对象会被外部访问到。8.3 什么时候考虑普通 setup需要兼容不支持script setup的旧工具链时。需要动态控制setup逻辑比如根据条件返回不同内容时。团队历史项目中大量使用普通setup且短期内不打算统一重构时。需要在同一个组件中对比两套写法、理解语法糖底层原理时。新项目默认优先考虑script setup它更简洁类型推导也更友好。9. 常见问题与排查方法问题现象可能原因排查方式解决方案模板中读取不到 setup 返回的数据返回值写漏了变量名拼写不一致检查 return 对象和模板变量名补全返回值确保命名一致模板中修改数据不更新视图返回的是普通对象不是 ref/reactive在 setup 中打印数据类型改用 ref 或 reactive模板中渲染出 [object Object]直接渲染了响应式代理对象检查模板绑定表达式绑定具体属性或使用 JSON.stringify 调试setup 中无法使用 this组合式 API 设计如此检查代码是否有 this用 ref/reactive 替代实例属性props 赋值报错props 是只读的查看控制台警告创建局部 ref 或 emit 到父组件父组件调用子组件方法失败子组件使用了 script setup 且未 defineExpose查看子组件代码添加 defineExpose渲染函数不生效setup 返回了函数且同时存在 template检查组件结构二选一使用 h 函数嵌套 reactive 对象中 ref 取不到值嵌套 ref 不会自动解包打印对象结构避免嵌套 ref或手动 .value普通 setup 返回的 ref 在模板中显示 undefined模板中误加了 .value检查模板表达式去掉 .value组件内事件绑定后不触发方法未 return检查 return 对象把方法加入返回值9.1 定位思路遇到setup返回值相关的问题先按照下面的步骤排查查看浏览器控制台是否有 Vue 警告。在setup末尾打console.log(returnValue)确认返回值内容。在模板中添加临时调试文本比如{{ count }}、{{ JSON.stringify(obj) }}。用 Vue Devtools 查看组件实例属性确认setup返回是否有效。如果涉及父子通信先确认子组件是否被父组件正确渲染再确认 emit 事件名是否一致。如果是script setup项目检查是否有defineExpose缺失。9.2 运行时依赖问题Vue3 项目的setup写法依赖组合式 API 相关模块比如vue/runtime-core。项目整体通过 Vite 或 Vue CLI 构建时一般不需要单独安装额外依赖。如果发现ref、reactive等函数导入失败优先检查npm ls vue以及package.json中 Vue 大版本是否为 3.x。10. 最佳实践与使用建议10.1 返回结构保持精简不要把所有内部变量都 return。模板里用不到的内部计算值、临时变量、工具函数不要暴露到渲染上下文中。这样可以减少模板命名冲突也让 Devtools 的组件树更清爽。10.2 逻辑按职责拆分setup越大返回值越难维护。建议把可复用逻辑提取成组合式函数// useCounter.js import { ref } from vue export function useCounter(initial 0) { const count ref(initial) function increment() { count.value } function decrement() { count.value-- } return { count, increment, decrement } }组件中这样使用script setup import { useCounter } from ./useCounter const { count, increment } useCounter(10) /script template p{{ count }}/p button clickincrement加 1/button /template这是setup返回值思路的自然延伸先在组合式函数内部组织逻辑再把需要渲染的数据和方法暴露给组件。10.3 优先使用 ref 还是 reactive小技巧单个基本类型值用ref一组关联属性用reactive。但如果团队偏好统一风格也可以全部使用ref并通过storeToRefs、toRefs等工具做结构转换。重点是保持一致不要让同一个组件里一半 ref 一半 reactive 且没有规则。10.4 与 TypeScript 结合普通setup返回对象时TypeScript 能自动推断返回值类型。使用script setup时类型体验更好可以直接定义script setup langts import { ref } from vue interface User { name: string age: number } const user refUser({ name: , age: 0 }) /script10.5 版本升级时的兼容性如果项目从 Vue2 迁移到 Vue3setup返回值是一个需要重点测试的部分。原有data、methods、computed、watch中的数据回填到setup返回对象时容易出现响应性丢失。建议迁移时先按最小案例跑通再逐步扩大业务范围。11. 总结与下一步setup的返回值本质上解决了一件事把组件的 JavaScript 逻辑层和模板渲染层连接起来。返回对象时模板可以通过变量名访问响应式数据、方法、计算属性返回函数时你可以完全接管渲染过程。理解返回值就理解了 Vue3 组合式 API 的骨架。建议先跑一遍 3 个最小案例返回 ref 和 reactive 对象、返回 computed 和方法、在子组件中通过 defineExpose 暴露方法给父组件。这三个案例能把 90% 日常开发中遇到的setup返回值场景覆盖到。最容易踩的坑是漏写返回值、模板中给 ref 加.value、以及script setup下没有使用defineExpose。这三个坑在排查时最值得优先检查。下一步可以继续深入的方向把setup返回值与provide/inject结合做跨层通信、用组合式函数整理复杂业务、把普通setup写法迁移到script setup并验证类型推导效果。学会返回值之后再看watch、computed、生命周期钩子在setup中的写法整个 Vue3 组件开发体系就串起来了。