<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__
  }
}

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

相关推荐
傻虎贼头贼脑5 分钟前
day21JS-npm中的部分插件使用方法
前端·npm·node.js
low神17 分钟前
前端在网络安全攻击问题上能做什么?
前端·安全·web安全
码力码力我爱你1 小时前
QT + WebAssembly + Vue环境搭建
vue.js·vue·wasm·webassembly·emscripten
qbbmnnnnnn1 小时前
【CSS Tricks】如何做一个粒子效果的logo
前端·css
唐家小妹1 小时前
【flex-grow】计算 flex弹性盒子的子元素的宽度大小
前端·javascript·css·html
涔溪1 小时前
uni-app环境搭建
前端·uni-app
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_032】5.4 Grid 网格布局的显示网格与隐式网格(上)
前端·css·css3·html5·网格布局·grid布局·css网格布局
洛千陨1 小时前
element-plus弹窗内分页表格保留勾选项
前端·javascript·vue.js
小小19921 小时前
elementui 单元格添加样式的两种方法
前端·javascript·elementui
完球了2 小时前
【Day02-JS+Vue+Ajax】
javascript·vue.js·笔记·学习·ajax