Vue3 script setup 的四层追问答到第三层才算过关

<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。

编译链路大致是这样:

flowchart LR A[SFC 单文件源码] --> B[parse 解析] B --> C[compileScript 处理 setup] C --> D[收集顶层绑定] D --> E[生成 setup 函数体] E --> F[模板编译为 render] F --> G[绑定内联进 render 闭包]

坑在这里:顶层 await 会让组件变成异步组件。 副作用是父组件必须用 <Suspense> 兜底,渲染时机被推迟,SSR 下问题更明显。

普通 script 和 script setup 的执行顺序,以及顶层 await 带来的时序变化:

flowchart TD A[普通 script 先执行] --> B[script setup 后执行] B --> C{是否有顶层 await} C -->|否| D[同步 setup 直接渲染] C -->|是| E[组件变为异步组件] E --> F[必须由 Suspense 接管]

传统 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()             // 能调到子组件方法
}

暴露的数据流这样走:

flowchart TD A[子组件 script setup] --> B[默认对外封闭] A --> C[defineExpose 收集] C --> D[挂到实例 exposeProxy] D --> E[父组件 ref.value 访问] E --> F[暴露 ref 自动解包 保持响应] E --> G[暴露 value 快照 丢响应]

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,还是解构丢失响应?评论区聊聊。

有用的话点个赞。

相关推荐
不一样的少年_10 小时前
别用前端思维写后端:一张 5MB 图片,为什么能撑爆内存?
前端·后端·图片资源
沐言人生10 小时前
太吊了!这 2 款网盘工具,让我省 4000 大洋!
前端
鬼手点金10 小时前
opencode-隐私优先配置
服务器·前端·javascript·bug·openclaw
涛涛ing10 小时前
4200万月活、85万行代码,Lovable用6个月把生产应用从Next.js迁到了TanStack Start
前端
悠仁さん10 小时前
【C++】auto
java·前端·c++
liangshanbo121510 小时前
面试题:React.memo 和 useMemo 的区别
前端·react.js·前端框架
苹果二10 小时前
使用Vue的指南
前端·javascript·vue.js
害人终害己11 小时前
‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。
前端·javascript·vue.js
被摘下的星星11 小时前
CSS 基础说明
前端·css
怕浪猫20 小时前
RAG 面试 6 连问,从原理到优化全部覆盖
python·算法·面试