找回密码
 立即注册
首页 业界区 业界 有点儿神奇,原来vue3的setup语法糖中组件无需注册因为 ...

有点儿神奇,原来vue3的setup语法糖中组件无需注册因为这个

袁曼妮 2025-6-6 15:25:26
前言

众所周知,在vue2的时候使用一个vue组件要么全局注册,要么局部注册。但是在setup语法糖中直接将组件import导入无需注册就可以使用,你知道这是为什么呢?注:本文中使用的vue版本为3.4.19。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
看个demo

我们先来看个简单的demo,代码如下:
  1. <template>
  2.   <Child />
  3. </template>
复制代码
上面这个demo在setup语法糖中import导入了Child子组件,然后在template中就可以直接使用了。
我们先来看看上面的代码编译后的样子,在之前的文章中已经讲过很多次如何在浏览器中查看编译后的vue文件,这篇文章就不赘述了。编译后的代码如下:
  1. import {<template>
  2.   <Child />
  3. </template>createBlock as _createBlock,<template>
  4.   <Child />
  5. </template>defineComponent as _defineComponent,<template>
  6.   <Child />
  7. </template>openBlock as _openBlock,} from "/node_modules/.vite/deps/vue.js?v=23bfe016";import Child from "/src/components/setupComponentsDemo/child.vue";const _sfc_main = _defineComponent({<template>
  8.   <Child />
  9. </template>__name: "index",<template>
  10.   <Child />
  11. </template>setup(__props, { expose: __expose }) {<template>
  12.   <Child />
  13. </template><template>
  14.   <Child />
  15. </template>__expose();<template>
  16.   <Child />
  17. </template><template>
  18.   <Child />
  19. </template>const __returned__ = { Child };<template>
  20.   <Child />
  21. </template><template>
  22.   <Child />
  23. </template>return __returned__;<template>
  24.   <Child />
  25. </template>},});function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {<template>
  26.   <Child />
  27. </template>return _openBlock(), _createBlock($setup["Child"]);}_sfc_main.render = _sfc_render;export default _sfc_main;
复制代码
从上面的代码可以看到,编译后setup语法糖已经没有了,取而代之的是一个setup函数。在setup函数中会return一个对象,对象中就包含了Child子组件。
有一点需要注意的是,我们原本是在setup语法糖中import导入的Child子组件,但是经过编译后import导入的代码已经被提升到setup函数外面去了。
在render函数中使用$setup["Child"]就可以拿到Child子组件,并且通过_createBlock($setup["Child"]);就可以将子组件渲染到页面上去。从命名上我想你应该猜到了$setup对象和上面的setup函数的return对象有关,其实这里的$setup["Child"]就是setup函数的return对象中的Child组件。至于在render函数中是怎么拿到setup函数返回的对象可以看我的另外一篇文章: Vue 3 的 setup语法糖到底是什么东西?
接下来我将通过debug的方式带你了解编译时是如何将Child塞到setup函数的return对象中,以及怎么将import导入Child子组件的语句提升到setup函数外面去的。
compileScript函数

在上一篇 有点东西,template可以直接使用setup语法糖中的变量原来是因为这个 文章中我们已经详细讲过了setup语法糖是如何编译成setup函数,以及如何根据将顶层绑定生成setup函数的return对象。所以这篇文章的重点是setup语法糖如何处理里面的import导入语句。
还是一样的套路启动一个debug终端。这里以vscode举例,打开终端然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。
1.png

然后在node_modules中找到vue/compiler-sfc包的compileScript函数打上断点,compileScript函数位置在/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js。接下来我们来看看简化后的compileScript函数源码,代码如下:
  1. function compileScript(sfc, options) {<template>
  2.   <Child />
  3. </template>const ctx = new ScriptCompileContext(sfc, options);<template>
  4.   <Child />
  5. </template>const setupBindings = Object.create(null);<template>
  6.   <Child />
  7. </template>const scriptSetupAst = ctx.scriptSetupAst;<template>
  8.   <Child />
  9. </template>for (const node of scriptSetupAst.body) {<template>
  10.   <Child />
  11. </template><template>
  12.   <Child />
  13. </template>if (node.type === "ImportDeclaration") {<template>
  14.   <Child />
  15. </template><template>
  16.   <Child />
  17. </template><template>
  18.   <Child />
  19. </template>// 。。。省略<template>
  20.   <Child />
  21. </template><template>
  22.   <Child />
  23. </template>}<template>
  24.   <Child />
  25. </template>}<template>
  26.   <Child />
  27. </template>for (const node of scriptSetupAst.body) {<template>
  28.   <Child />
  29. </template><template>
  30.   <Child />
  31. </template>// 。。。省略<template>
  32.   <Child />
  33. </template>}<template>
  34.   <Child />
  35. </template>let returned;<template>
  36.   <Child />
  37. </template>const allBindings = {<template>
  38.   <Child />
  39. </template><template>
  40.   <Child />
  41. </template>...setupBindings,<template>
  42.   <Child />
  43. </template>};<template>
  44.   <Child />
  45. </template>for (const key in ctx.userImports) {<template>
  46.   <Child />
  47. </template><template>
  48.   <Child />
  49. </template>if (!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate) {<template>
  50.   <Child />
  51. </template><template>
  52.   <Child />
  53. </template><template>
  54.   <Child />
  55. </template>allBindings[key] = true;<template>
  56.   <Child />
  57. </template><template>
  58.   <Child />
  59. </template>}<template>
  60.   <Child />
  61. </template>}<template>
  62.   <Child />
  63. </template>returned = `{ `;<template>
  64.   <Child />
  65. </template>for (const key in allBindings) {<template>
  66.   <Child />
  67. </template><template>
  68.   <Child />
  69. </template>// ...遍历allBindings对象生成setup函数的返回对象<template>
  70.   <Child />
  71. </template>}<template>
  72.   <Child />
  73. </template>return {<template>
  74.   <Child />
  75. </template><template>
  76.   <Child />
  77. </template>// ...省略<template>
  78.   <Child />
  79. </template><template>
  80.   <Child />
  81. </template>content: ctx.s.toString(),<template>
  82.   <Child />
  83. </template>};}
复制代码
我们先来看看简化后的compileScript函数。
在compileScript函数中首先使用ScriptCompileContext类new了一个ctx上下文对象,在new的过程中将compileScript函数的入参sfc传了过去,sfc中包含了<script setup>模块的位置信息以及源代码。
ctx.scriptSetupAst是<script setup>模块中的code代码字符串对应的AST抽象语法树。
接着就是遍历AST抽象语法树的内容,如果发现当前节点是一个import语句,就会将该import收集起来放到ctx.userImports对象中(具体如何收集接下来会讲)。
然后会再次遍历AST抽象语法树的内容,如果发现当前节点上顶层声明的变量、函数、类、枚举声明,就将其收集到setupBindings对象中。
最后就是使用扩展运算符...setupBindings将setupBindings对象中的属性合并到allBindings对象中。
对于ctx.userImports的处理就不一样了,不会将其全部合并到allBindings对象中。而是遍历ctx.userImports对象,如果当前import导入不是ts的类型导入,并且导入的东西在template模版中使用了,才会将其合并到allBindings对象中。
经过前面的处理allBindings对象中已经收集了setup语法糖中的所有顶层绑定,然后遍历allBindings对象生成setup函数中的return对象。
我们在debug终端来看看生成的return对象,如下图:
2.png

从上图中可以看到setup函数中已经有了一个return对象了,return对象的Child属性值就是Child子组件的引用。
收集import导入

接下来我们来详细看看如何将setup语法糖中的全部import导入收集到ctx.userImports对象中,代码如下:
  1. function compileScript(sfc, options) {<template>
  2.   <Child />
  3. </template>// 。。。省略<template>
  4.   <Child />
  5. </template>for (const node of scriptSetupAst.body) {<template>
  6.   <Child />
  7. </template><template>
  8.   <Child />
  9. </template>if (node.type === "ImportDeclaration") {<template>
  10.   <Child />
  11. </template><template>
  12.   <Child />
  13. </template><template>
  14.   <Child />
  15. </template>hoistNode(node);<template>
  16.   <Child />
  17. </template><template>
  18.   <Child />
  19. </template><template>
  20.   <Child />
  21. </template>for (let i = 0; i < node.specifiers.length; i++) {<template>
  22.   <Child />
  23. </template><template>
  24.   <Child />
  25. </template><template>
  26.   <Child />
  27. </template><template>
  28.   <Child />
  29. </template>// 。。。省略<template>
  30.   <Child />
  31. </template><template>
  32.   <Child />
  33. </template><template>
  34.   <Child />
  35. </template>}<template>
  36.   <Child />
  37. </template><template>
  38.   <Child />
  39. </template>}<template>
  40.   <Child />
  41. </template>}<template>
  42.   <Child />
  43. </template>// 。。。省略}
复制代码
遍历scriptSetupAst.body也就是<script setup>模块中的code代码字符串对应的AST抽象语法树,如果当前节点类型是import导入,就会执行hoistNode函数将当前import导入提升到setup函数外面去。
hoistNode函数

将断点走进hoistNode函数,代码如下:
  1. function hoistNode(node) {<template>
  2.   <Child />
  3. </template>const start = node.start + startOffset;<template>
  4.   <Child />
  5. </template>let end = node.end + startOffset;<template>
  6.   <Child />
  7. </template>while (end<template>
  8.   <Child />
  9. </template>{<template>
  10.   <Child />
  11. </template>return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ""));};
复制代码
camelize函数使用正则表达式将kebab-case命名法,转换为首字母为小写的驼峰命名法。比如my-component经过camelize函数的处理后就变成了myComponent。这也就是为什么以 myComponent 为名注册的组件,在模板中可以通过  或  引用。
再来看第二个ids.add(capitalize(camelize(tag)))方法,经过camelize函数的处理后已经变成了首字母为小写的小驼峰命名法,然后执行capitalize函数。代码如下:
  1. const capitalize = (str) => {<template>
  2.   <Child />
  3. </template>return str.charAt(0).toUpperCase() + str.slice(1);};
复制代码
capitalize函数的作用就是将首字母为小写的驼峰命名法转换成首字母为大写的驼峰命名法。这也就是为什么以 MyComponent 为名注册的组件,在模板中可以通过 、或者是  引用。
我们这个场景中是使用引用子组件,所以set集合中就会收集Child。再回到isImportUsed函数,代码如下:
  1. function isImportUsed(local, sfc) {<template>
  2.   <Child />
  3. </template>return resolveTemplateUsedIdentifiers(sfc).has(local);}
复制代码
前面讲过了local变量的值是Child,resolveTemplateUsedIdentifiers(sfc)返回的是包含Child的set集合,所以resolveTemplateUsedIdentifiers(sfc).has(local)的值是true。也就是isUsedInTemplate变量的值是true,表示当前import导入变量是在template中使用。后面生成return对象时判断是否要将当前import导入加到return对象中,会去读取ctx.userImports[key].isUsedInTemplate属性,其实就是这个isUsedInTemplate变量。
总结

执行compileScript函数会将setup语法糖编译成setup函数,在compileScript函数中会去遍历<script setup>对应的AST抽象语法树。
如果是顶层变量、函数、类、枚举声明,就会将其收集到setupBindings对象中。
如果是import语句,就会将其收集到ctx.userImports对象中。还会根据import导入的信息判断当前import导入是否是ts的类型导入,并且赋值给isType属性。然后再去递归遍历template模块对应的AST抽象语法树,看import导入的变量是否在template中使用,并且赋值给isUsedInTemplate属性。
遍历setupBindings对象和ctx.userImports对象中收集的所有顶层绑定,生成setup函数中的return对象。在遍历ctx.userImports对象的时候有点不同,会去判断当前import导入不是ts的类型导入并且在还在template中使用了,才会将其加到setup函数的return对象中。在我们这个场景中setup函数会返回{ Child }对象。
在render函数中使用$setup["Child"]将子组件渲染到页面上去,而这个$setup["Child"]就是在setup函数中返回的Child属性,也就是Child子组件的引用。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
3.jpeg


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册