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> 先执行,用来声明 nameinheritAttrs 这类 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 解构出来当普通变量用。

refreactive 在两种写法里响应式行为完全一致------同一套响应式系统,差别只在暴露方式,不在响应式本身。

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

有用的话点个赞。

相关推荐
前端柱子1 小时前
Chaikin‘s Corner Cutting 算法 应用canvas绘制平滑的曲线
前端
gnip1 小时前
uts 插件示例:获取设备电量信息
前端·javascript
码上成长1 小时前
Mapbox 上用 Turf 裁多边形:屏幕贴边了,接口却说越界
前端·前端框架
计算机魔术师1 小时前
纽约时报告了OpenAI和微软:820万条聊天记录背后,是AI时代最大的版权保卫战
前端
晚安日记wanna1 小时前
一条 SMEMBERS 干瘫 Redis 节点:单线程的真正边界在哪
redis·后端·面试
码事漫谈1 小时前
在 Kubernetes 上管好数据库:金仓 KES-Operator 正式落地
前端·后端
six_1 小时前
RiverPlusCOM Web:不用安装的在线串口调试助手
vue.js·程序员
用户1880687493521 小时前
手上全是油,我怎么让rokid眼镜在厨房里教我炒菜
前端
gnip1 小时前
uni-app 原生插件
前端·javascript·uni-app