<script setup> 是编译期改写,不是运行时黑魔法。四层追问:绑定为什么能内联、编译链路长什么样、运行时差在哪、defineExpose 的暴露边界。多数人卡在第三层。
面试室里坐着个两年经验的前端,简历写着"熟练掌握 Vue3 组合式 API"。我问 <script setup> 了解多少,他张口就来:写代码更简洁,不用 return,开发很爽。
我接着问底层编译原理、对比传统 setup 的优势、组件属性暴露。他当场卡住。
这道题是 Vue3 面试的第一道分水岭。下面四层追问,能答到第三层才算合格。
不 return 模板也能拿到变量,但别停在这层
传统 setup 函数里,变量和方法必须 return 出去,模板才能访问。
js
export default {
setup() {
const count = ref(0)
function add() { count.value++ }
return { count, add } // 不 return,模板就看不到
}
}
<script setup> 不用手动 return,内部变量直接给模板用:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() { count.value++ }
</script>
<template>
<button @click="add">{{ count }}</button>
</template>
为什么不 return 模板也能拿到变量? 编译期把绑定直接内联进了 render 函数的作用域,压根不走运行时那套"返回对象"。
还有个常被问的点:<script setup> 能不能和普通 <script> 共存?能。普通 <script> 先执行,用来声明 name、inheritAttrs 这类 options;<script setup> 后执行。
它是编译器语法糖,不是运行时黑魔法
这一点三年开发也常说不清:编译器会把 <script setup> 里的代码编译成组件 setup 函数的内容,仅此而已。
编译阶段干三件事:
- 顶层变量、函数自动暴露给模板;
import进来的组件不需要在components里注册,编译阶段自动注册;- 模板直接引用闭包里的绑定,不经过
this。
编译链路大致是这样:
坑在这里:顶层 await 会让组件变成异步组件。 副作用是父组件必须用 <Suspense> 兜底,渲染时机被推迟,SSR 下问题更明显。
普通 script 和 script setup 的执行顺序,以及顶层 await 带来的时序变化:
传统 setup 返回的对象,和 <script setup> 编译产出,在实例上的形态完全不同------前者挂到 $$setup 走代理,后者是纯闭包。
少写一个 return,差在闭包和代理上
说两者"只是少写 return"是最大的误区。差在哪:
| 维度 | 传统 setup | script setup |
|---|---|---|
| 返回对象 | 每次实例化都构造并代理 | 编译期确定,闭包直取 |
| this 指向 | setup 内为 undefined,Options API 指向组件代理 | 内部根本没有 this |
| 作用域 | 返回对象挂在实例上,外部可探测 | 顶层绑定封闭在闭包,外部默认拿不到 |
| 运行时开销 | 有返回对象与代理开销 | 编译阶段优化,开销更低 |
闭包陷阱迁移到 <script setup> 一般不会复现 ,因为两者都是同一套闭包模型。真正容易翻车的是把 ref.value 解构出来当普通变量用。
ref、reactive 在两种写法里响应式行为完全一致------同一套响应式系统,差别只在暴露方式,不在响应式本身。
defineExpose:线上最容易栽的地方
父组件通过模板 ref 拿子组件实例。<script setup> 默认对外全封闭 ,内部属性一个都取不到,必须用 defineExpose 显式暴露。
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() { count.value = 0 }
defineExpose({ count, reset }) // 推荐:暴露 ref 本身
</script>
父组件访问:
js
const childRef = ref(null)
function handle() {
console.log(childRef.value.count) // 自动解包,保持响应
childRef.value.reset() // 能调到子组件方法
}
暴露的数据流这样走:
defineExpose 能暴露 ref、reactive、函数、普通值、组件实例。几个高频坑:
- 暴露
count和暴露count.value不是一回事。 前者父组件访问会自动解包且保持响应;后者是快照,丢了响应式。 - 忘记写
defineExpose,父组件childRef.value拿到的实例上方法全是undefined。先查子组件有没有显式暴露。 - 混合写法 下,普通
<script>先执行,defineExpose在<script setup>里生效。 - 不要把响应式数据解构后再暴露 ,会丢响应;也不要在
defineExpose里放持有大对象的响应式闭包,容易内存泄漏。
面试官考的不是你会不会写这个语法糖,而是你知不知道为什么它能这么写。同样一个 <script setup>,有人只会复制粘贴写业务,有人能聊编译、聊实例暴露、聊线上踩坑。
写在最后
回到开头:你觉得自己真的吃透 <script setup> 了吗?
给个具体的排查建议。下次遇到父组件 ref 拿不到子组件方法,先确认子组件有没有 defineExpose,再确认暴露的到底是 ref 还是 ref.value------大概率就是这两个里栽的。
你们在项目里用 <script setup> 踩过最离谱的坑是什么?是顶层 await 撞上 Suspense,还是解构丢失响应?评论区聊聊。
有用的话点个赞。