当前位置:首页 > 科技  > 软件

有点东西,Template可以直接使用Setup语法糖中的变量原来是因为这个

来源: 责编: 时间:2024-06-14 17:40:20 90观看
导读前言我们每天写vue3代码的时候都会使用到setup语法糖,那你知道为什么setup语法糖中的顶层绑定可以在template中直接使用的呢?setup语法糖是如何编译成setup函数的呢?本文将围绕这些问题带你揭开setup语法糖的神秘面纱。

前言

我们每天写vue3代码的时候都会使用到setup语法糖,那你知道为什么setup语法糖中的顶层绑定可以在template中直接使用的呢?setup语法糖是如何编译成setup函数的呢?本文将围绕这些问题带你揭开setup语法糖的神秘面纱。注:本文中使用的vue版本为3.4.19。LKs28资讯网——每日最新资讯28at.com

看个demo

看个简单的demo,代码如下:LKs28资讯网——每日最新资讯28at.com

<template>  <h1>{{ msg }}</h1>  <h2>{{ format(msg) }}</h2>  <h3>{{ title }}</h3>  <Child /></template><script lang="ts" setup>import { ref } from "vue";import Child from "./child.vue";import { format } from "./util.js";const msg = ref("Hello World!");let title;if (msg.value) {  const innerContent = "xxx";  console.log(innerContent);  title = "111";} else {  title = "222";}</script>

在上面的demo中定义了四个顶层绑定:Child子组件、从util.js文件中导入的format方法、使用ref定义的msg只读常量、使用let定义的title变量。并且在template中直接使用了这四个顶层绑定。LKs28资讯网——每日最新资讯28at.com

由于innerContent是在if语句里面的变量,不是<script setup>中的顶层绑定,所以在template中是不能使用innerContent的。LKs28资讯网——每日最新资讯28at.com

但是你有没有想过为什么<script setup>中的顶层绑定就能在template中使用,而像innerContent这种非顶层绑定就不能在template中使用呢?LKs28资讯网——每日最新资讯28at.com

我们先来看看上面的代码编译后的样子,在之前的文章中已经讲过很多次如何在浏览器中查看编译后的vue文件,这篇文章就不赘述了。编译后的代码如下:LKs28资讯网——每日最新资讯28at.com

import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=23bfe016";import { ref } from "/node_modules/.vite/deps/vue.js?v=23bfe016";import Child from "/src/components/setupDemo2/child.vue";import { format } from "/src/components/setupDemo2/util.js";const _sfc_main = _defineComponent({  __name: "index",  setup(__props, { expose: __expose }) {    __expose();    const msg = ref("Hello World!");    let title;    if (msg.value) {      const innerContent = "xxx";      console.log(innerContent);      title = "111";    } else {      title = "222";    }    const __returned__ = {      msg,      get title() {        return title;      },      set title(v) {        title = v;      },      Child,      get format() {        return format;      },    };    return __returned__;  },});function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {  // ...省略}_sfc_main.render = _sfc_render;export default _sfc_main;

从上面的代码中可以看到编译后已经没有了<script setup>,取而代之的是一个setup函数,这也就证明了为什么说setup是一个编译时语法糖。LKs28资讯网——每日最新资讯28at.com

setup函数的参数有两个,第一个参数为组件的 props。第二个参数为Setup 上下文对象,上下文对象暴露了其他一些在 setup 中可能会用到的值,比如:expose等。LKs28资讯网——每日最新资讯28at.com

再来看看setup函数中的内容,其实和我们的源代码差不多,只是多了一个return。使用return会将组件中的那四个顶层绑定暴露出去,所以在template中就可以直接使用<script setup>中的顶层绑定。LKs28资讯网——每日最新资讯28at.com

值的一提的是在return对象中title变量和format函数有点特别。title、format这两个都是属于访问器属性,其他两个msg、Child属于常见的数据属性。LKs28资讯网——每日最新资讯28at.com

title是一个访问器属性,同时拥有get 和 set,读取title变量时会走进get中,当给title变量赋值时会走进set中。LKs28资讯网——每日最新资讯28at.com

format也是一个访问器属性,他只拥有get ,调用format函数时会走进get中。由于他没有set,所以不能给format函数重新赋值。其实这个也很容易理解,因为format函数是从util.js文件中import导入的,当然不能给他重新赋值。LKs28资讯网——每日最新资讯28at.com

至于在template中是怎么拿到setup函数返回的对象可以看我的另外一篇文章: Vue 3 的 setup语法糖到底是什么东西?LKs28资讯网——每日最新资讯28at.com

看到这里有的小伙伴会有疑问了,不是还有一句import { ref } from "vue"也是顶层绑定,为什么里面的ref没有在setup函数中使用return暴露出去呢?还有在return对象中是如何将title、format识别为访问器属性呢?LKs28资讯网——每日最新资讯28at.com

在接下来的文章中我会逐一解答这些问题。LKs28资讯网——每日最新资讯28at.com

compileScript函数

在之前的 通过debug搞清楚.vue文件怎么变成.js文件文章中已经讲过了vue的script模块中的内容是由@vue/compiler-sfc包中的compileScript函数处理的,当然你没看过那篇文章也不会影响这篇文章的阅读。LKs28资讯网——每日最新资讯28at.com

首先我们需要启动一个debug终端。这里以vscode举例,打开终端然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

然后在node_modules中找到vue/compiler-sfc包的compileScript函数打上断点,compileScript函数位置在/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js。接下来我们先看看简化后的compileScript函数源码。LKs28资讯网——每日最新资讯28at.com

简化后的compileScript函数

在debug终端上面执行yarn dev后在浏览器中打开对应的页面,比如:http://localhost:5173/ 。此时断点就会走到compileScript函数中,在我们这个场景中简化后的compileScript函数代码如下:LKs28资讯网——每日最新资讯28at.com

function compileScript(sfc, options) {  // ---- 第一部分 ----  // 根据<script setup>中的内容生成一个ctx上下文对象  // 在ctx上下文对象中拥有一些属性和方法  const ctx = new ScriptCompileContext(sfc, options);  const { source, filename } = sfc;  // 顶层声明的变量、函数组成的对象  const setupBindings = Object.create(null);  // script标签中的内容开始位置和结束位置  const startOffset = ctx.startOffset;  const endOffset = ctx.endOffset;  // script setup中的内容编译成的AST抽象语法树  const scriptSetupAst = ctx.scriptSetupAst;  // ---- 第二部分 ----  // 遍历<script setup>中的内容,处理里面的import语句、顶层变量、函数、类、枚举声明还有宏函数  for (const node of scriptSetupAst.body) {    if (node.type === "ImportDeclaration") {      // ...省略    }  }  for (const node of scriptSetupAst.body) {    if (      (node.type === "VariableDeclaration" ||        node.type === "FunctionDeclaration" ||        node.type === "ClassDeclaration" ||        node.type === "TSEnumDeclaration") &&      !node.declare    ) {      // 顶层声明的变量、函数、类、枚举声明组成的setupBindings对象      // 给setupBindings对象赋值,{msg: 'setup-ref'}      // 顶层声明的变量组成的setupBindings对象      walkDeclaration(        "scriptSetup",        node,        setupBindings,        vueImportAliases,        hoistStatic      );    }  }  // ---- 第三部分 ----  // 移除template中的内容和script的开始标签  ctx.s.remove(0, startOffset);  // 移除style中的内容和script的结束标签  ctx.s.remove(endOffset, source.length);  // ---- 第四部分 ----  // 将<script setup>中的顶层绑定的元数据存储到ctx.bindingMetadata对象中  // 为什么要多此一举存储一个bindingMetadata对象呢?答案是setup的return的对象有时会直接返回顶层变量,有时会返回变量的get方法,有时会返回变量的get和set方法,  // 所以才需要一个bindingMetadata对象来存储这些顶层绑定的元数据。  for (const [key, { isType, imported, source: source2 }] of Object.entries(    ctx.userImports  )) {    if (isType) continue;    ctx.bindingMetadata[key] =      imported === "*" ||      (imported === "default" && source2.endsWith(".vue")) ||      source2 === "vue"        ? "setup-const"        : "setup-maybe-ref";  }  for (const key in setupBindings) {    ctx.bindingMetadata[key] = setupBindings[key];  }  // 生成setup方法的args参数;  let args = `__props`;  const destructureElements =    ctx.hasDefineExposeCall || !options.inlineTemplate      ? [`expose: __expose`]      : [];  if (destructureElements.length) {    args += `, { ${destructureElements.join(", ")} }`;  }  // ---- 第五部分 ----  // 根据<script setup>中的顶层绑定生成return对象中的内容  let returned;  const allBindings = {    ...setupBindings,  };  for (const key in ctx.userImports) {    // 不是引入ts中的类型并且import导入的变量还需要在template中使用    if (!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate) {      allBindings[key] = true;    }  }  returned = `{ `;  for (const key in allBindings) {    if (      allBindings[key] === true &&      ctx.userImports[key].source !== "vue" &&      !ctx.userImports[key].source.endsWith(".vue")    ) {      returned += `get ${key}() { return ${key} }, `;    } else if (ctx.bindingMetadata[key] === "setup-let") {      const setArg = key === "v" ? `_v` : `v`;      returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;    } else {      returned += `${key}, `;    }  }  returned = returned.replace(/, $/, "") + ` }`;  ctx.s.appendRight(    endOffset,    `const __returned__ = ${returned}Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })return __returned__}`  );  // ---- 第六部分 ----  // 生成setup函数  ctx.s.prependLeft(    startOffset,    `${genDefaultAs} /*#__PURE__*/${ctx.helper(      `defineComponent`    )}({${def}${runtimeOptions}${hasAwait ? `async ` : ``}setup(${args}) {${exposeCall}`  );  ctx.s.appendRight(endOffset, `})`);  // ---- 第七部分 ----  // 插入import vue语句  if (ctx.helperImports.size > 0) {    ctx.s.prepend(      `import { ${[...ctx.helperImports]        .map((h) => `${h} as _${h}`)        .join(", ")} } from 'vue'`    );  }  return {    // ...省略    bindings: ctx.bindingMetadata,    imports: ctx.userImports,    content: ctx.s.toString(),  };}

首先我们来看看compileScript函数的第一个参数sfc对象,在之前的文章 vue文件是如何编译为js文件 中我们已经讲过了sfc是一个descriptor对象,descriptor对象是由vue文件编译来的。LKs28资讯网——每日最新资讯28at.com

descriptor对象拥有template属性、scriptSetup属性、style属性,分别对应vue文件的<template>模块、<script setup>模块、<style>模块。LKs28资讯网——每日最新资讯28at.com

在我们这个场景只关注scriptSetup属性,sfc.scriptSetup.content的值就是<script setup>模块中code代码字符串,LKs28资讯网——每日最新资讯28at.com

sfc.source的值就是vue文件中的源代码code字符串。sfc.scriptSetup.loc.start.offset为<script setup>中内容开始位置,sfc.scriptSetup.loc.end.offset为<script setup>中内容结束位置。详情查看下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

我们再来看compileScript函数中的内容,在compileScript函数中包含了从<script setup>语法糖到setup函数的完整流程。乍一看可能比较难以理解,所以我将其分为七块。LKs28资讯网——每日最新资讯28at.com

  • 根据<script setup>中的内容生成一个ctx上下文对象。
  • 遍历<script setup>中的内容,处理里面的import语句、顶层变量、顶层函数、顶层类、顶层枚举声明等。
  • 移除template和style中的内容,以及script的开始标签和结束标签。
  • 将<script setup>中的顶层绑定的元数据存储到ctx.bindingMetadata对象中。
  • 根据<script setup>中的顶层绑定生成return对象。
  • 生成setup函数定义
  • 插入import vue语句

在接下来的文章中我将逐个分析这七块的内容。LKs28资讯网——每日最新资讯28at.com

生成ctx上下文对象

我们来看第一块的代码,如下:LKs28资讯网——每日最新资讯28at.com

// 根据<script setup>中的内容生成一个ctx上下文对象// 在ctx上下文对象中拥有一些属性和方法const ctx = new ScriptCompileContext(sfc, options);const { source, filename } = sfc;// 顶层声明的变量、函数组成的对象const setupBindings = Object.create(null);// script标签中的内容开始位置和结束位置const startOffset = ctx.startOffset;const endOffset = ctx.endOffset;// script setup中的内容编译成的AST抽象语法树const scriptSetupAst = ctx.scriptSetupAst;

在这一块的代码中主要做了一件事,使用ScriptCompileContext构造函数new了一个ctx上下文对象。在之前的 为什么defineProps宏函数不需要从vue中import导入?文章中我们已经讲过了ScriptCompileContext构造函数里面的具体代码,这篇文章就不赘述了。LKs28资讯网——每日最新资讯28at.com

本文只会讲用到的ScriptCompileContext类中的startOffset、endOffset、scriptSetupAst、userImports、helperImports、bindingMetadata、s等属性。LKs28资讯网——每日最新资讯28at.com

  • startOffset、endOffset属性是在ScriptCompileContext类的constructor构造函数中赋值的。其实就是sfc.scriptSetup.loc.start.offset和sfc.scriptSetup.loc.end.offset,<script setup>中内容开始位置和<script setup>中内容结束位置,只是将这两个字段塞到ctx上下文中。
  • scriptSetupAst是在ScriptCompileContext类的constructor构造函数中赋值的,他是<script setup>模块的代码转换成的AST抽象语法树。在ScriptCompileContext类的constructor构造函数中会调用@babel/parser包的parse函数,以<script setup>中的code代码字符串为参数生成AST抽象语法树。
  • userImports在new一个ctx上下文对象时是一个空对象,用于存储import导入的顶层绑定内容。
  • helperImports同样在new一个ctx上下文对象时是一个空对象,用于存储需要从vue中import导入的函数。
  • bindingMetadata同样在new一个ctx上下文对象时是一个空对象,用于存储所有的import顶层绑定和变量顶层绑定的元数据。
  • s属性是在ScriptCompileContext类的constructor构造函数中赋值的,以vue文件中的源代码code字符串为参数new了一个MagicString对象赋值给s属性。

magic-string是由svelte的作者写的一个库,用于处理字符串的JavaScript库。它可以让你在字符串中进行插入、删除、替换等操作,并且能够生成准确的sourcemap。LKs28资讯网——每日最新资讯28at.com

MagicString对象中拥有toString、remove、prependLeft、appendRight等方法。s.toString用于生成返回的字符串,我们来举几个例子看看这几个方法你就明白了。LKs28资讯网——每日最新资讯28at.com

s.remove( start, end )用于删除从开始到结束的字符串:LKs28资讯网——每日最新资讯28at.com

const s = new MagicString('hello word');s.remove(0, 6);s.toString(); // 'word'

s.prependLeft( index, content )用于在指定index的前面插入字符串:LKs28资讯网——每日最新资讯28at.com

const s = new MagicString('hello word');s.prependLeft(5, 'xx');s.toString(); // 'helloxx word'

s.appendRight( index, content )用于在指定index的后面插入字符串:LKs28资讯网——每日最新资讯28at.com

const s = new MagicString('hello word');s.appendRight(5, 'xx');s.toString(); // 'helloxx word'

除了上面说的那几个属性,在这里定义了一个setupBindings变量。初始值是一个空对象,用于存储顶层声明的变量、函数等。LKs28资讯网——每日最新资讯28at.com

遍历

将断点走到第二部分,代码如下:LKs28资讯网——每日最新资讯28at.com

for (const node of scriptSetupAst.body) {  if (node.type === "ImportDeclaration") {    // ...省略  }}for (const node of scriptSetupAst.body) {  if (    (node.type === "VariableDeclaration" ||      node.type === "FunctionDeclaration" ||      node.type === "ClassDeclaration" ||      node.type === "TSEnumDeclaration") &&    !node.declare  ) {    // 顶层声明的变量、函数、类、枚举声明组成的setupBindings对象    // 给setupBindings对象赋值,{msg: 'setup-ref'}    // 顶层声明的变量组成的setupBindings对象    walkDeclaration(      "scriptSetup",      node,      setupBindings,      vueImportAliases,      hoistStatic    );  }}

在这一部分的代码中使用for循环遍历了两次scriptSetupAst.body,scriptSetupAst.body为script中的代码对应的AST抽象语法树中body的内容,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

从上图中可以看到scriptSetupAst.body数组有6项,分别对应的是script模块中的6块代码。LKs28资讯网——每日最新资讯28at.com

第一个for循环中使用if判断node.type === "ImportDeclaration",也就是判断是不是import语句。如果是import语句,那么import的内容肯定是顶层绑定,需要将import导入的内容存储到ctx.userImports对象中。注:后面会专门写一篇文章来讲如何收集所有的import导入。LKs28资讯网——每日最新资讯28at.com

通过这个for循环已经将所有的import导入收集到了ctx.userImports对象中了,在debug终端看看此时的ctx.userImports,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

从上图中可以看到在ctx.userImports中收集了三个import导入,分别是Child组件、format函数、ref函数。LKs28资讯网——每日最新资讯28at.com

在里面有几个字段需要注意,isUsedInTemplate表示当前import导入的东西是不是在template中使用,如果为true那么就需要将这个import导入塞到return对象中。LKs28资讯网——每日最新资讯28at.com

isType表示当前import导入的是不是type类型,因为在ts中是可以使用import导入type类型,很明显type类型也不需要塞到return对象中。LKs28资讯网——每日最新资讯28at.com

我们再来看第二个for循环,同样也是遍历scriptSetupAst.body。如果当前是变量定义、函数定义、类定义、ts枚举定义,这四种类型都属于顶层绑定(除了import导入以外就只有这四种顶层绑定了)。需要调用walkDeclaration函数将这四种顶层绑定收集到setupBindings对象中。LKs28资讯网——每日最新资讯28at.com

从前面的scriptSetupAst.body图中可以看到if模块的type为IfStatement,明显不属于上面的这四种类型,所以不会执行walkDeclaration函数将里面的innerContent变量收集起来后面再塞到return对象中。这也就解释了为什么非顶层绑定不能在template中直接使用。LKs28资讯网——每日最新资讯28at.com

我们在debug终端来看看执行完第二个for循环后setupBindings对象是什么样的,如下图:LKs28资讯网——每日最新资讯28at.com

从上图中可以看到在setupBindings对象中收集msg和title这两个顶层变量。其中的setup-ref表示当前变量是一个ref定义的变量,setup-let表示当前变量是一个let定义的变量。LKs28资讯网——每日最新资讯28at.com

移除template模块和style模块

接着将断点走到第三部分,代码如下:LKs28资讯网——每日最新资讯28at.com

ctx.s.remove(0, startOffset);ctx.s.remove(endOffset, source.length);

这块代码很简单,startOffset为<script setup>中的内容开始位置,endOffset为<script setup>中的内容结束位置,ctx.s.remove方法为删除字符串。LKs28资讯网——每日最新资讯28at.com

所以ctx.s.remove(0, startOffset)的作用是:移除template中的内容和script的开始标签。LKs28资讯网——每日最新资讯28at.com

ctx.s.remove(endOffset, source.length)的作用是:移除style中的内容和script的结束标签。LKs28资讯网——每日最新资讯28at.com

我们在debug终端看看执行这两个remove方法之前的code代码字符串是什么样的,如下图:LKs28资讯网——每日最新资讯28at.com

从上图中可以看到此时的code代码字符串和我们源代码差不多,唯一的区别就是那几个import导入已经被提取到script标签外面去了(这个是在前面第一个for循环处理import导入的时候处理的)。LKs28资讯网——每日最新资讯28at.com

将断点走到执行完这两个remove方法之后,在debug终端看看此时的code代码字符串,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

从上图中可以看到执行这两个remove方法后template模块、style模块(虽然本文demo中没有写style模块)、script开始标签、script结束标签都已经被删除了。唯一剩下的就是script模块中的内容,还有之前提出去的那几个import导入。LKs28资讯网——每日最新资讯28at.com

将顶层绑定的元数据存储到ctx.bindingMetadata

接着将断点走到第四部分,代码如下:LKs28资讯网——每日最新资讯28at.com

for (const [key, { isType, imported, source: source2 }] of Object.entries(  ctx.userImports)) {  if (isType) continue;  ctx.bindingMetadata[key] =    imported === "*" ||    (imported === "default" && source2.endsWith(".vue")) ||    source2 === "vue"      ? "setup-const"      : "setup-maybe-ref";}for (const key in setupBindings) {  ctx.bindingMetadata[key] = setupBindings[key];}// 生成setup函数的args参数;let args = `__props`;const destructureElements =  ctx.hasDefineExposeCall || !options.inlineTemplate    ? [`expose: __expose`]    : [];if (destructureElements.length) {  args += `, { ${destructureElements.join(", ")} }`;}

上面的代码主要分为三块,第一块为for循环遍历前面收集到的ctx.userImports对象。这个对象里面收集的是所有的import导入,将所有import导入塞到ctx.bindingMetadata对象中。LKs28资讯网——每日最新资讯28at.com

第二块也是for循环遍历前面收集的setupBindings对象,这个对象里面收集的是顶层声明的变量、函数、类、枚举,同样的将这些顶层绑定塞到ctx.bindingMetadata对象中。LKs28资讯网——每日最新资讯28at.com

为什么要多此一举存储一个ctx.bindingMetadata对象呢?LKs28资讯网——每日最新资讯28at.com

答案是setup的return的对象有时会直接返回顶层变量(比如demo中的msg常量)。有时只会返回变量的访问器属性 get(比如demo中的format函数)。有时会返回变量的访问器属性 get和set(比如demo中的title变量)。所以才需要一个ctx.bindingMetadata对象来存储这些顶层绑定的元数据。LKs28资讯网——每日最新资讯28at.com

将断点走到执行完这两个for循环的地方,在debug终端来看看此时收集的ctx.bindingMetadata对象是什么样的,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

最后一块代码也很简单进行字符串拼接生成setup函数的参数,第一个参数为组件的props、第二个参数为expose方法组成的对象。如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

生成return对象

接着将断点走到第五部分,代码如下:LKs28资讯网——每日最新资讯28at.com

let returned;const allBindings = {  ...setupBindings,};for (const key in ctx.userImports) {  // 不是引入ts中的类型并且import导入的变量还需要在template中使用  if (!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate) {    allBindings[key] = true;  }}returned = `{ `;for (const key in allBindings) {  if (    allBindings[key] === true &&    ctx.userImports[key].source !== "vue" &&    !ctx.userImports[key].source.endsWith(".vue")  ) {    returned += `get ${key}() { return ${key} }, `;  } else if (ctx.bindingMetadata[key] === "setup-let") {    const setArg = key === "v" ? `_v` : `v`;    returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;  } else {    returned += `${key}, `;  }}returned = returned.replace(/, $/, "") + ` }`;ctx.s.appendRight(  endOffset,  `  const __returned__ = ${returned}  Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })  return __returned__  }  `);

这部分的代码看着很多,其实逻辑也非常清晰,我也将其分为三块。LKs28资讯网——每日最新资讯28at.com

在第一块中首先使用扩展运算符...setupBindings将setupBindings对象中的属性合并到allBindings对象中,因为setupBindings对象中存的顶层声明的变量、函数、类、枚举都需要被return出去。LKs28资讯网——每日最新资讯28at.com

然后遍历ctx.userImports对象,前面讲过了ctx.userImports对象中存的是所有的import导入(包括从vue中import导入ref函数)。在循环里面执行了if判断!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate,这个判断的意思是如果当前import导入的不是ts的type类型并且import导入的内容在template模版中使用了。才会去执行allBindings[key] = true,执行后就会将满足条件的import导入塞到allBindings对象中。LKs28资讯网——每日最新资讯28at.com

后面生成setup函数的return对象就是通过遍历这个allBindings对象实现的。这也就解释了为什么从vue中import导入的ref函数也是顶层绑定,为什么他没有被setup函数返回。因为只有在template中使用的import导入顶层绑定才会被setup函数返回。LKs28资讯网——每日最新资讯28at.com

将断点走到遍历ctx.userImports对象之后,在debug终端来看看此时的allBindings对象是什么样的,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

从上图中可以看到此时的allBindings对象中存了四个需要return的顶层绑定。LKs28资讯网——每日最新资讯28at.com

接着就是执行for循环遍历allBindings对象生成return对象的字符串,这循环中有三个if判断条件。我们先来看第一个,代码如下:LKs28资讯网——每日最新资讯28at.com

if (  allBindings[key] === true &&  ctx.userImports[key].source !== "vue" &&  !ctx.userImports[key].source.endsWith(".vue")) {  returned += `get ${key}() { return ${key} }, `;}

if条件判断是:如果当前import导入不是从vue中,并且也不是import导入一个vue组件。那么就给return一个只拥有get的访问器属性,对应我们demo中的就是import { format } from "./util.js"中的format函数。LKs28资讯网——每日最新资讯28at.com

我们再来看第二个else if判断,代码如下:LKs28资讯网——每日最新资讯28at.com

else if (ctx.bindingMetadata[key] === "setup-let") {  const setArg = key === "v" ? `_v` : `v`;  returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;}

这个else if条件判断是:如果当前顶层绑定是一个let定义的变量。那么就给return一个同时拥有get和set的访问器属性,对应我们demo中的就是let title"变量。LKs28资讯网——每日最新资讯28at.com

最后就是else,代码如下:LKs28资讯网——每日最新资讯28at.com

else {  returned += `${key}, `;}

这个else中就是普通的数据属性了,对应我们demo中的就是msg变量和Child组件。LKs28资讯网——每日最新资讯28at.com

将断点走到生成return对象之后,在debug终端来看看此时生成的return对象是什么样的,如下图:LKs28资讯网——每日最新资讯28at.com

从上图中可以看到此时已经生成了return对象啦。LKs28资讯网——每日最新资讯28at.com

前面我们只生成了return对象,但是还没将其插入到要生成的code字符串中,所以需要执行ctx.s.appendRight方法在末尾插入return的代码。LKs28资讯网——每日最新资讯28at.com

将断点走到执行完ctx.s.appendRight方法后,在debug终端来看看此时的code代码字符串是什么样的,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

从上图中可以看到此时的code代码字符串中多了一块return的代码。LKs28资讯网——每日最新资讯28at.com

生成setup函数定义

接着将断点走到第六部分,代码如下:LKs28资讯网——每日最新资讯28at.com

ctx.s.prependLeft(  startOffset,  `${genDefaultAs} /*#__PURE__*/${ctx.helper(    `defineComponent`  )}({${def}${runtimeOptions}${hasAwait ? `async ` : ``}setup(${args}) {${exposeCall}`);ctx.s.appendRight(endOffset, `})`);

这部分的代码很简单,调用ctx.s.prependLeft方法从左边插入一串代码。插入的这串代码就是简单的字符串拼接,我们在debug终端来看看要插入的代码是什么样的,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

是不是觉得上面这块需要插入的代码看着很熟悉,他就是编译后的_sfc_main对象除去setup函数内容的部分。将断点走到ctx.s.appendRight方法执行之后,再来看看此时的code代码字符串是什么样的,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

从上图中可以看到此时的setup函数基本已经生成完了。LKs28资讯网——每日最新资讯28at.com

插入import vue语句

上一步生成的code代码字符串其实还有一个问题,在代码中使用了_defineComponent函数,但是没有从任何地方去import导入。LKs28资讯网——每日最新资讯28at.com

第七块的代码就会生成缺少的import导入,代码如下:LKs28资讯网——每日最新资讯28at.com

if (ctx.helperImports.size > 0) {  ctx.s.prepend(    `import { ${[...ctx.helperImports]      .map((h) => `${h} as _${h}`)      .join(", ")} } from 'vue'`  );}

将断点走到ctx.s.prepend函数执行后,再来看看此时的code代码字符串,如下图:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

从上图中可以看到已经生成了完整的setup函数啦。LKs28资讯网——每日最新资讯28at.com

总结

整个流程图如下:LKs28资讯网——每日最新资讯28at.com

图片图片LKs28资讯网——每日最新资讯28at.com

  • 遍历<script setup>中的代码将所有的import导入收集到ctx.userImports对象中。
  • 遍历<script setup>中的代码将所有的顶层变量、函数、类、枚举收集到setupBindings对象中。
  • 调用ctx.s.remove方法移除template、style模块以及script开始标签和结束标签。
  • 遍历前面收集的ctx.userImports和setupBindings对象,将所有的顶层绑定元数据存储到bindingMetadata对象中。
  • 遍历前面收集的ctx.userImports和setupBindings对象,生成return对象中的内容。在这一步的时候会将没有在template中使用的import导入给过滤掉,这也就解释了为什么从vue中导入的ref函数不包含在return对象中。
  • 调用ctx.s.prependLeft方法生成setup的函数定义。
  • 调用ctx.s.prepend方法生成完整的setup函数。

本文链接:http://www.28at.com/showinfo-26-93866-0.html有点东西,Template可以直接使用Setup语法糖中的变量原来是因为这个

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 从 Prometheus 到 OpenTelemetry:指标监控的演进与实践

下一篇: 为什么要推荐使用现代化PHP框架?

标签:
  • 热门焦点
Top