ARTICLE DETAIL

资讯详情

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

Vue3 setup函数返回值全解析:对象、渲染函数与Promise

Vue3 setup函数返回值全解析:对象、渲染函数与Promise 这次我们聊 Vue3 的 setup 函数。网上关于 setup 的教程不少但大多数只告诉你“setup 返回一个对象模板就能用”至于返回值的类型有哪些、分别解决什么问题、为什么 Vue3 要这么设计、和我们写了好几年的 Options API 到底差别在哪真正讲透的内容其实不多。这篇文章把这几个点一次说清楚并且直接用代码演示不看那些只讲概念的视频和文章。先说结论setup 函数的返回值一共有三种合法情况——返回对象、返回函数渲染函数、以及配合 Suspense 的返回 Promise。其中返回对象是最常用的返回函数用在自定义渲染和 JSX 场景返回 Promise 则用于异步 setup。文章后面会用可运行的 Demo 把三种情况逐一验证还会顺带讲清楚 setup 与 script setup 语法糖的关系、常见面试题和工程上的最佳实践。适合的读者正在从 Vue2 转向 Vue3 的开发者、面试前需要梳理 Composition API 的人、以及已经在写 Vue3 但只看过零散教程、想系统搞清楚 setup 返回值机制的工程师。1. setup 返回值的核心能力速览先把最关键的信息放在前面方便快速建立印象。能力项说明返回值类型一返回一个普通对象对象中的 ref、reactive、函数都会暴露给模板使用返回值类型二返回一个函数该函数作为渲染函数执行替代 template 编译返回值类型三返回 Promise配合 Suspense 组件实现异步 setup加载期间可展示 fallback触发时机setup 在组件实例创建之前执行先于 beforeCreatethis 指向setup 内部 this 为 undefined访问组件实例需要借助 getCurrentInstance入参props 和 contextcontext 包含 attrs、slots、emit、expose语法糖script setup中顶层变量自动暴露等价于 setup 返回对象适合场景逻辑复用、组合式封装、动态渲染、异步数据加载、JSX 开发API 能力支持通过 expose 控制对外暴露的属性和方法便于父组件调用子组件方法从这张表能看出setup 不是简单地把 data 和 methods 换个写法它承担的是组件逻辑初始化的统一入口。返回值的设计直接决定了模板能访问什么、渲染函数怎么写、异步加载怎么接。2. setup 函数到底是什么2.1 执行时机与组件生命周期setup 是 Vue3 组合式 API 的入口函数它在组件实例创建之前执行。也就是说setup 执行的时候组件实例还没有完全初始化所以你看不到beforeCreate和created这两个生命周期钩子在 setup 之前。更准确地说setup 就是替代了这两个钩子的职责。一个组件的执行顺序通常是props 初始化 - setup 执行 - beforeCreate - created - 模板渲染这里要注意beforeCreate和created仍然存在只是如果你使用了 setup这两者的很多逻辑都可以直接写在 setup 里。Vue3 没有删除 Options API 的生命周期钩子但在组合式 API 中对应的是onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted这套函数。2.2 为什么 setup 里不能用 this很多 Vue2 老手刚接触 setup 时最容易栽在这在 setup 里写this.xxx结果拿到的是 undefined。原因在于 setup 执行阶段组件实例并不存在。this 的指向依赖于函数的调用方式在 setup 阶段组件实例尚未创建Vue 也就没有把 this 绑定到任何实例上。这是刻意设计的目的就是让逻辑不依赖 this避免 Options API 中 this 指向混乱、类型推断困难的问题。如果需要访问组件实例可以使用getCurrentInstanceimport { getCurrentInstance } from vue export default { setup() { const instance getCurrentInstance() console.log(当前组件实例, instance) return {} } }但在大多数业务代码里直接用 props、context 和组合式 API 就够了getCurrentInstance一般只在封装库或者特殊调试场景下才用。2.3 setup 的两个参数setup 函数接收两个参数setup(props, context) { // props: 组件接收的 props响应式对象 // context: attrs、slots、emit、expose }props 是响应式的它由父组件传入并且会随父组件更新。需要注意不要对 props 做解构因为解构会让字段失去响应性。如果想解构后仍然保持响应需要用toRefs或toRefimport { toRefs } from vue export default { props: { title: String, count: Number }, setup(props) { // 错误做法const { title, count } props会丢失响应性 const { title, count } toRefs(props) console.log(title.value, count.value) return { title, count } } }context 不是响应式对象可以直接解构使用export default { setup(props, { attrs, slots, emit, expose }) { // attrs: 非 props 的属性如 class、style 等 // slots: 插槽内容 // emit: 触发事件 // expose: 控制子组件暴露给父组件的内容 emit(update:visible, true) return {} } }expose是 Vue3 新增的能力用来控制通过模板 ref 拿到组件实例时能看到哪些东西。在 setup 返回对象时返回的属性和方法默认会全部暴露给父组件如果你不想暴露全部就可以用expose手动收窄范围。3. setup 返回对象的完整解析3.1 返回值如何暴露给模板这是 setup 最常见的用法。setup 返回一个普通对象对象中的属性和方法会被合并到渲染上下文中模板可以直接使用。template div classcard p姓名{{ name }}/p p年龄{{ age }}/p p职业{{ job }}/p button clickchangeName修改姓名/button button clickincrement年龄 1/button /div /template script import { ref, reactive } from vue export default { setup() { // 使用 ref 定义基本类型响应式数据 const name ref(张三) const age ref(28) // 使用 reactive 定义对象类型响应式数据 const userInfo reactive({ job: 前端工程师 }) const changeName () { name.value 李四 userInfo.job 高级前端工程师 } const increment () { age.value 1 } return { name, age, job: userInfo.job, changeName, increment } } } /script这里有两个细节值得注意第一模板中写{{ name }}而不是{{ name.value }}。因为 Vue3 的模板编译会自动对顶层 ref 做解包。但回到 setup 内部你必须写name.value才能读取和修改 ref 的值。第二上面返回了job: userInfo.job这其实是把 reactive 对象中的某个属性单独暴露出去。但这里存在响应性丢失的问题返回的job是字符串原始值它和userInfo.job不再是引用关系。在模板中展示没问题但如果后续修改userInfo.job模板中的job不会更新。正确做法是返回整个 userInfo或者用toRef来创建 refimport { toRef } from vue export default { setup() { const userInfo reactive({ job: 前端工程师 }) // 使用 toRef 保持响应性 const jobRef toRef(userInfo, job) return { userInfo, job: jobRef } } }3.2 ref、reactive 与普通属性的差异返回对象中可以直接放普通变量吗可以。模板能访问但普通变量没有响应性。export default { setup() { // 普通变量初始渲染可以显示但后续修改不会触发更新 let message Hello Vue3 const updateMessage () { message Updated Message console.log(值已经修改, message) } return { message, updateMessage } } }如果点击按钮调用 updateMessage控制台会打印新值但页面不会更新。这是因为 Vue 的响应式系统只追踪 ref、reactive 这类代理对象普通变量的赋值操作无法触发依赖更新。所以返回对象时有三个层次返回类型模板可用性响应性使用建议ref 变量模板自动解包响应式基本类型优先用 refreactive 对象模板中保持对象引用响应式对象、数组、嵌套结构优先用 reactive普通变量可访问但不会触发更新无仅用于一次性常量或非响应数据3.3 返回计算属性setup 里可以返回 computed 计算属性语法上它也是一个 refimport { ref, computed } from vue export default { setup() { const firstName ref(张) const lastName ref(三) const fullName computed(() ${firstName.value}${lastName.value}) return { firstName, lastName, fullName } } }模板中的fullName不需要加.valueVue 会自动解包。这也是 setup 返回值设计上的一个好处不论内部是 ref、reactive 还是 computed模板里统一按普通属性访问。3.4 返回对象时最容易踩的坑返回对象本身很简单但有几个实际问题容易出现。第一个坑同时返回了重名属性。setup 返回对象的键名如果和 props、data、methods 中的名称冲突优先级和覆盖关系容易让人困惑。通常 setup 返回的属性会覆盖 data 中的同名属性但强烈不建议在同一个组件里混用 Options API 和 Composition API 的重名属性排查起来很痛苦。第二个坑把整个 props 返回出去。直接return { props }虽然可以让模板访问 props但 props 本身已经可以在模板中直接使用没必要多此一举。如果直接解构 props 再返回又会丢失响应性。第三个坑setup 返回对象中 this 不能指向组件实例。如果你在返回的某个方法里写了this.name这里的 this 指向的是调用方法的上下文比如模板事件绑定的组件实例而不是 setup 内部。更安全的做法是始终通过闭包变量访问响应式数据。4. setup 返回函数自定义渲染场景4.1 返回 h 函数setup 的第二种返回值是函数。当 setup 返回一个函数时这个函数会被作为组件的渲染函数来使用此时组件不需要写 template编译器也不会去编译模板。这是动态渲染组件的一种实现方式。script import { h } from vue export default { name: DynamicHeading, props: { level: { type: Number, default: 1 }, content: { type: String, default: } }, setup(props) { // setup 返回函数该函数即渲染函数 return () { return h(h${props.level}, { class: dynamic-heading }, props.content) } } } /script这个组件的功能是根据levelprop 动态渲染 h1 到 h6。如果用模板写需要写六个 v-if用渲染函数只需要一行h调用。h 函数的基本签名是h(type, propsOrChildren, children)type标签名、组件对象或异步组件propsOrChildren属性、事件、class、style 等children子节点可以是字符串、数组、插槽返回渲染函数的 setup 在 UI 组件库的源码里很常见比如生成表格列、动态表单字段、菜单树这类需要程序化构建 DOM 的场景。4.2 返回 JSX 函数如果你配置了 Vite 的 JSX 插件或者使用 Vue CLI Babel 的 JSX 支持setup 也可以直接返回 JSXscript import { ref } from vue export default { name: CounterJsx, setup() { const count ref(0) const increment () { count.value 1 } return () ( div classcounter p当前计数{count.value}/p button onClick{increment}1/button /div ) } } /script注意JSX 环境下需要显式写count.value因为不会像模板那样自动解包。这一点经常有人搞混。返回函数的本质是把渲染逻辑完全交给开发者。它的优势是灵活劣势是放弃了模板编译器的解析能力直接写 h 函数或 JSX 的可读性比模板差。日常业务组件并不建议都用这种方式只有在封装底层组件、动态渲染场景才值得用。5. setup 返回 Promise异步加载与 Suspense5.1 async setup 的使用条件setup 的第三种返回类型是 Promise。当你把 setup 声明为 async 函数时它可以等待异步操作完成后再返回// 异步 setup async setup() { const response await fetch(/api/user) const user await response.json() return { user } }但这里有一个关键限制不写 Suspense 会直接报警告。Vue 需要父组件中存在Suspense来感知这个异步 setup 的状态在 Promise 完成之前展示 fallback 内容完成之后自动切换到真正的组件内容。官方给出的警告信息类似setup() returned a Promise, but no Suspense was found in the parent tree.所以 async setup 必须配合 Suspense 使用。5.2 完整异步组件示例下面是一个完整的例子父组件用 Suspense 包裹一个异步加载的子组件子组件在 setup 中请求数据并返回。先看父组件template div classasync-wrapper h2用户列表异步组件/h2 Suspense template #default AsyncUserList / /template template #fallback div classloading数据加载中.../div /template /Suspense /div /template script import { defineAsyncComponent } from vue // 动态导入异步组件 const AsyncUserList defineAsyncComponent(() import(./AsyncUserList.vue)) export default { name: AsyncWrapper, components: { AsyncUserList } } /script再看子组件template ul classuser-list li v-foruser in users :keyuser.id {{ user.name }} - {{ user.email }} /li /ul /template script export default { name: AsyncUserList, async setup() { // 模拟异步请求 const fetchUsers () { return new Promise((resolve) { setTimeout(() { resolve([ { id: 1, name: 张三, email: zhangsanexample.com }, { id: 2, name: 李四, email: lisiexample.com }, { id: 3, name: 王五, email: wangwuexample.com } ]) }, 1500) }) } const users await fetchUsers() return { users } } } /script访问页面时Suspense 会先显示 fallback 中的“数据加载中...”等setup中的 Promise resolve 之后再渲染真实的用户列表。整个过程不需要在 onMounted 里手动维护 loading 状态这是 async setup 最大的价值把异步加载和组件渲染状态从业务代码里剥离出去。不过要注意生产项目中建议把接口请求封装成组合式函数composables不要在 setup 里堆一大段请求逻辑。6. 从 setup 返回值理解 script setup 语法糖Vue3.2 之后推荐使用script setup它本质上是 setup 返回对象的语法糖。在script setup中定义的所有顶层变量、函数、import 内容都会自动暴露给模板不需要写 return。template div p{{ name }}/p p{{ count }}/p button clickincrement1/button /div /template script setup import { ref } from vue import { getUserInfo } from /api/user const name ref(张三) const count ref(0) const increment () { count.value 1 } // 模板中可以直接使用 name、count、increment /script这里的等价关系是script setup const count ref(0) /script 等价于 script export default { setup() { const count ref(0) return { count } } } /script还有一个重要区别在script setup中如果要让父组件通过模板 ref 访问到子组件的方法需要显式使用defineExpose。因为script setup默认是关闭模板 ref 访问子组件内部状态的避免组件内部细节意外暴露给外部。script setup import { ref } from vue const innerValue ref(内部数据) const exposedMethod () { console.log(父组件调用了 exposedMethod) } // 只暴露 exposedMethod不暴露 innerValue defineExpose({ exposedMethod }) /script而普通 setup 中返回对象默认会被视为对外暴露内容。如果你希望限制暴露需要在 setup 中使用 context.expose 手动控制export default { setup(props, { expose }) { const innerValue ref(内部数据) const exposedMethod () { console.log(父组件调用了 exposedMethod) } // 只暴露 exposedMethod 给父组件 expose({ exposedMethod }) return { innerValue } } }这个区别在封装组件库、二次封装业务组件时非常关键不要漏掉。7. 功能验证搭一个最小 Demo 跑一遍7.1 创建 Vite Vue3 项目先准备一个本地项目命令如下npm create vitelatest vue3-setup-demo -- --template vue如果你用的是 pnpmpnpm create vite vue3-setup-demo --template vue进入项目并安装依赖cd vue3-setup-demo npm install npm run dev浏览器访问http://localhost:5173之后把默认的 App.vue 和 HelloWorld.vue 替换成下面的代码。7.2 验证返回对象响应式数据与模板访问创建一个SetupReturnDemo.vuetemplate div classsetup-demo h2setup 返回对象示例/h2 p姓名{{ name }}/p p年龄{{ age }}/p p职业{{ job }}/p p普通变量{{ normalMessage }}/p button clickchangeName修改姓名/button button clickincrement年龄 1/button button clickupdateNormal修改普通变量/button /div /template script import { ref, reactive } from vue export default { name: SetupReturnDemo, setup() { const name ref(张三) const age ref(28) const userInfo reactive({ job: 前端工程师 }) // 普通变量无响应性 let normalMessage 普通变量初始值 const changeName () { name.value 李四 userInfo.job 高级前端工程师 } const increment () { age.value 1 } const updateNormal () { normalMessage 修改后的普通变量 console.log(普通变量值变为, normalMessage) } return { name, age, job: userInfo.job, normalMessage, changeName, increment, updateNormal } } } /script在 App.vue 中引入并渲染这个组件。验证要点点击“修改姓名”姓名和职业都会变化说明 ref 和 reactive 的响应性都生效。点击“修改普通变量”页面文字不会变化但控制台会打印新值说明普通变量不触发视图更新。修改job: userInfo.job的返回方式后你会发现只有重新渲染时 job 才会同步。这正是前面强调的“返回普通属性会断开响应性链接”的实际表现。7.3 验证返回函数动态标题渲染创建一个DynamicHeading.vuescript import { h } from vue export default { name: DynamicHeading, props: { level: { type: Number, default: 1 }, content: { type: String, default: } }, setup(props) { return () { return h( h${props.level}, { class: dynamic-heading, style: { color: #1677ff } }, props.content ) } } } /script在 App.vue 中template div DynamicHeading :level1 content一级标题 / DynamicHeading :level2 content二级标题 / DynamicHeading :level3 content三级标题 / /div /template script import DynamicHeading from ./components/DynamicHeading.vue export default { components: { DynamicHeading } } /script验证要点页面出现三个不同级别的蓝色标题说明 setup 返回的渲染函数生效。你可以试着把 level 换成 4、5、6标题级别也会跟着变。7.4 验证 async setupSuspense 异步加载按 5.2 小节的代码创建AsyncWrapper.vue和AsyncUserList.vue在 App.vue 中引入 AsyncWrapper。验证要点首次进入页面时应该先看到“数据加载中...”的 fallback。延迟 1.5 秒后fallback 自动切换为用户列表。浏览器控制台没有setup() returned a Promise, but no Suspense警告。如果出现警告检查 AsyncUserList 是否被 Suspense 包裹以及 AsyncWrapper 是否确实引入了 Suspense 组件。7.5 初始化耗时与性能观察在浏览器打开 DevTools 的 Performance 面板记录从页面刷新到组件渲染完成的时间。把 setup 中放入耗时同步任务比如一个 100ms 的 for 循环观察渲染阻塞情况。从这个实验能直观看到setup 中的同步代码会阻塞组件渲染。因此不要把大计算量的任务直接写在 setup 顶部应该放到 onMounted、或者使用异步 setup Suspense 来延迟加载也可以用 computed 做惰性计算。同时打开 Vue DevTools 的 Components 面板选中某个组件后可以查看setupState里面就是 setup 返回的对象。这有助于调试如果模板中某个变量显示为 undefined先去 setupState 里确认它是否真的被返回了。8. setup 返回值相关面试题与常见报错8.1 高频面试题问题 1setup 和 created 谁先执行setup 先执行。setup 在组件实例创建之前执行created 是在实例创建完成后触发的生命周期钩子所以从时序上看 setup 更早。问题 2为什么 setup 中不能使用 this因为 setup 执行时组件实例尚未创建this 没有可绑定的实例对象。这是 Vue3 的设计决策目的是让逻辑不依赖 this从而改善类型推导和逻辑复用。问题 3setup 返回的对象和 data 有什么区别setup 返回的是普通对象其中 ref 和 reactive 会被 Vue 的响应式系统处理data 本身也是 Vue 内部通过 reactive 处理的。从模板使用角度看两者都能提供响应式数据区别主要在于组织方式data 是扁平属性集合setup 返回值可以来自组合式函数、可以包含函数和计算属性逻辑组织更灵活。问题 4setup 中如何访问 props通过第一个参数。props 是响应式对象可以直接在模板中使用但不要解构解构会丢失响应性。问题 5async setup 能不能直接使用不能。setup 返回 Promise 时父组件必须用 Suspense 包裹否则 Vue 无法知道异步状态控制台会报警告组件内容也不会正常渲染。问题 6script setup 和普通 setup 的返回值逻辑一样吗本质上一样。script setup中顶层变量自动暴露给模板等价于 setup 返回对象。唯一需要注意的是普通 setup 中 setup 是一个导出属性而script setup是编译期语法同一组件不能混用这两种写法。8.2 常见报错排查问题现象可能原因排查方式解决方案setup 中使用 this 得到 undefinedsetup 执行时组件实例不存在检查是否误用 this.setup使用 props、context、getCurrentInstance模板中变量显示为 undefined变量没有在 return 中导出或拼写不一致打开 Vue DevTools 查看 setupState补全 return或在script setup中定义顶层变量ref 在模板中显示为对象而不是值模板中没有自动解包或者误将 ref 嵌套进了普通对象检查是否是{{ obj.refValue }}的嵌套场景直接返回 ref 本身或使用 reactive 包一层setup 返回 Promise 后页面空白缺少 Suspense 包裹检查父组件是否引入 Suspense外层包Suspense提供 default 和 fallback渲染函数返回后显示空白h 函数参数写错或者 return 的是函数本身而不是调用结果检查 setup 是否写成了return h(...)而不是return () h(...)确认返回的是函数而不是 VNodeprops 解构后页面不更新解构导致响应性丢失检查是否直接const { title } props使用 toRefs 或直接用 props.title父组件无法调用子组件方法script setup 默认关闭对外暴露检查子组件是否定义了 defineExpose用 defineExpose 显式暴露方法9. 最佳实践与工程建议9.1 优先使用 script setup但不要盲从script setup减少了模板和逻辑之间的跳转成本代码量也更小新项目完全可以直接用。但是普通 setup 并没有被废弃如果你在做组件库为了兼容 Vue2 迁移过来的项目或者需要动态控制 setup 的返回内容普通 setup 仍然有用。理解两种写法的对应关系比单纯背语法更重要。9.2 返回值保持最小暴露setup 返回值中只暴露模板确实需要的内容。如果一个变量只在 setup 内部使用就没必要放进 return 对象里。这样做的直接好处是模板上下文更干净、组件对外暴露的接口更明确、调试时快速定位变量来源。对于script setup来说所有顶层变量都会暴露到模板所以定义时就要注意命名和用途避免把临时变量、工具函数都扔在顶层。9.3 用组合式函数收拢逻辑不要把所有逻辑都塞进 setup 函数体。setup 可以返回很多内容但 setup 函数本身不应该无限膨胀。推荐把某一类逻辑拆成组合式函数比如用户信息、权限判断、列表分页然后用 setup 统一组装。// useUserInfo.js import { ref } from vue import { fetchUserInfo } from /api/user export function useUserInfo() { const userInfo ref(null) const loading ref(false) const loadUserInfo async () { loading.value true try { userInfo.value await fetchUserInfo() } finally { loading.value false } } return { userInfo, loading, loadUserInfo } }然后在组件里script setup import { useUserInfo } from /composables/useUserInfo const { userInfo, loading, loadUserInfo } useUserInfo() loadUserInfo() /script模板中直接使用 userInfo、loading 即可。这种写法下setup 返回值变成了组合式函数的“出口”逻辑本身可以被多个组件复用。9.4 异步数据加载优先考虑 Suspense async setup以前在 Vue2 中加载异步数据常见的做法是 data 中定义 loading 状态onMounted 里请求接口再在 then 中手动关闭 loading。Vue3 的 Suspense 让这个过程组件化。多个异步组件可以挂在同一个 Suspense 下统一等待统一展示 fallback。不过要注意如果项目使用 SSRSuspense 的处理会更复杂可能需要结合服务端数据预取来做。纯客户端项目可以先从单个异步组件开始用。9.5 重视响应式边界setup 返回值是开发者在组合式 API 中最常接触的边界之一ref、reactive、computed 是响应式数据普通变量不是props 解构会丢失响应性reactive 对象中取出的普通属性也会断开响应性。每次从 setup 中返回内容时先问一句这到底是不是响应式的这个习惯能省掉大量排查时间。10. 总结与下一步setup 函数是 Vue3 组合式 API 的入口返回值设计也没有那么玄返回对象给模板用返回函数做自定义渲染返回 Promise 配合 Suspense 做异步加载。更关键的是理解背后的设计原因为什么 this 不可用、为什么返回值要区分响应性和非响应性、为什么 script setup 能简化开发。把这套逻辑理清楚之后再去看 Vue3 的源码或者组件库的实现会顺畅很多。如果你刚开始学习 Vue3建议最先动手验证两个功能一是 setup 返回 ref、reactive、普通变量时模板中的表现差异二是 async setup 配合 Suspense 的加载流程。前者帮你建立响应式边界的直觉后者帮你掌握 Vue3 异步组件的标准姿势。最容易踩的坑就是 props 解构丢失响应性和 async setup 忘写 Suspense这两个问题在面试和实际开发中都经常出现。下一步可以继续研究组合式函数的封装规范、Vue3 响应式源码中 ref 和 reactive 的实现差异、Suspense 在服务端渲染中的行为、以及script setup下defineOptions、defineModel等新特性。想系统掌握 Vue3从 setup 返回值这个点切入然后把周边知识点逐个打通是最快的路径。
返回列表