<script setup>是如何编译的

认识setup

好久之前就想写一篇文章聊一聊setup,对于接触过vue3的小伙伴来说,应该都用过setup的方式编写过组件,这种方式也是vue推荐的默认的单文件组件书写方式。这种方式的优点官网也总结的很好了,一个是可以将相关的逻辑写到一块,一个是可以实现复用逻辑的抽离,详见官方文档

setup本身是作为一个组件选项而存在,但是如果只使用setup选项写代码也是完全可以的,也就是常用的<script setup>语法,我们就先聊聊scriptSetup是怎么编译的

从complier-sfc说起

上篇文章我们也提到了,vue仓库中是有这个complier-sfc包的,这个包的作用就是为vue提供单文件组件的编译支持,不仅仅是模版的编译,还涉及到script和style。链接在这。 这个功能是通过complier-sfc暴露的parse这个api实现的,会解析模版、script(setup)和style块。解析出来的script块大概是这个样子: 其中的attr,也就是我们在script标签上附加的东西,lang和setup,content是scriptSetup中的源代码,loc是ast的一部分,用来表明代码的起始位置,这个对于生成sourcemap是有用的。

这只是第一步,我们拿到了scriptSetup块中写的代码,还需要经过complier-sfc包中的compilerScript函数的编译,这个过程是一个ast级别的操作,compilerScript会先把我们上面的scriptSetup中的content转换为js的ast,这个过程是用babel来实现的,babel暴露了一个parse函数(和compiler-sfc是两回事),可以将JS转为AST,虽然日常使用我们配置babel做语法降级居多,但是babel最根本的能力是对JS进行AST操作,各种babel插件都是基于AST的增删改实现的。

推荐小伙伴去看一下babel的handbook,隐藏的很深,babel文档上找到不到他,但是对于理解JS代码很有帮助:

github.com/jamiebuilds...

处理编译时代码

说回正题,我们知道<script setup>语法是有一些编译时代码的,比如defineProps,defineEmits,defineExpose,那么在这个过程中,defineExpose会被转换为_expose,编译后到代码都会被拼接到setup组件选项的上下文了,_expose其实就是setup执行时的第二个参数解构出来的expose,就像这样:

javascript 复制代码
// 最终生成的代码
const _sfc_main {
    //...
  setup(props, { _expose }) {
     
    // 我们的代码
    
    _expose()
    
    // ...
  }
}

那么这个过程是怎么确定要找的代码呢?答案是AST函数调用的AST是CallExpressiton,只需要找到所有顶层代码的函数调用中的CallExpression中对应的标识符是"defineExpose"就行了。对于比如defineProps和defineEmits要稍微特殊些,因为AST也告诉了我们代码的开始和结束位置,vue会根据位置信息将他俩删除,对于emit需要像上面调用emit,emit的参数可以通过AST拿到,这个后面会用于组件选项对象的拼接。 然后将emit返回,这样emit就可以在模版中使用,类似于$emit。

javascript 复制代码
const _sfc_main {
    //...
  setup(props, { emit }) {

    // 我们的代码
    
    emit()
    
    // ...
  }
}

对于defineProps的情况会特殊些,因为defineProps的返回值我们会在模版中使用,如果defineProps有返回值,我们在AST中可以看到返回值的loc,也就是起始位置,compiler会将这个标识符的部分直接切下来: ,这样我们就得到了defineProps的返回值,但是我们还需要defineProps的调用时的参数,方便后面拼接组件选项props字符串,这个参数也可以通过AST拿到,也就是defineProps的arguments,最后处理方式依然类似于emit和expose,但这次代码会拼接在最前面:

javascript 复制代码
const _sfc_main {
    //...
  setup(_props) {
    // 或者解构,如const {a, b} = _props(['a','b'])
    const props = _props(['a','b'])

    // 我们的代码
    // ...
  }
}

也就是说,我们就算先使用defineProps的返回值,后调用defineProps,也是没关系的,并不会因为变量先使用后声明就报错。 小伙伴们可以自己去ASTExpoloer体验一下,试一试JS代码和AST的对应关系。

拼接剩余部分

对于编译时代码处理就到此为止了,接下来还需要拼接一些额外的代码,比如组件选项声明,"const _sfc_main = { ",然后拼接组件的名字,也就是我们的.vue文件的文件名,然后拼接props和emit,之前也已经通过defineProps和defineEmits的arguments拿到了,直接拼上就可以,所以现在我们的代码大概类似于这样:

javascript 复制代码
const _sfc_main {
  name:xx,
  props:xx,
  emit:xx,
  setup(_props,{_expose,emit}) {
    // _props,emit,_expose取决于代码有没有调用
    // 我们的代码
    // ...
  }
}

最后还需要拼接上返回值,那么返回值都有什么呢?对于scriptSetup来说就是函数顶层声明,所以可以遍历AST中的所有顶层声明,无论是函数声明,类声明还是变量声明,获取到我们要返回到值,当然这里面也包括了defineEmits和defineProps的返回值。 最后拼接出的代码大概这样:

javascript 复制代码
const _sfc_main {
  name:xx,
  props:xx,
  emit:xx,
  setup(_props,{expose,emit}) {
    // _props,emit,expose取决于代码有没有调用
    // 我们的代码
    // ...
    
   const __returned__ = xx 
   Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
   return __returned__
  }
}

也就是我们在浏览器中看到的样子: 最终是一个组件选项,文章很多地方还有疏漏,如果有疑问欢迎留言

相关推荐
崔庆才丨静觅30 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax