vue3中script标签的setup实现原理

2 篇文章 0 订阅
1 篇文章 0 订阅

概述

当 vue3 新建组件时,我们有两种选择选项式和组合式,如下所示

  • 传统方式

    <script>
    import { ref } from "vue";
    export default {
      setup() {
        const count = ref(0);
        const handleClick = () => {
          count.value++;
        };
        return { count, handleClick };
      },
    };
    </script>
    <template>
      <h1>传统的sctipt setup</h1>
      <button @click="handleClick">Count++</button>
      <p>{{ count }}</p>
    </template>
    <style scoped></style>
    
  • 组合式

<script setup>
import { ref } from "vue";
const count = ref(0);

const handleClick = () => {
  count.value++;
};
</script>
<template>
  <h1>组合式sctipt setup</h1>
  <button @click="handleClick">Count++</button>
  <p>{{ count }}</p>
</template>
<style scoped></style>

很明显,第二种方式 Componsition API的设计思想更加简洁,这样可以使用类似于React Hooks的方式编写 vue 组件。而<script> 标签中的自定义属性或关键字setup正是Composition API中的一个重要概念。
setup函数可以用于设置组件的初始状态、响应式数据、计算属性、定义事件交互等,当<script>标签使用setup关键字时,相当于也是在定义该 vue 组件的setup函数,并将其中的内容最终暴露给模板使用。

setup详细介绍

setup原理

下面是<script setup>的一些内部原理:

  • Single File Component (SFC) 解析:Vue 的编译器首先会解析 .vue 单文件组件,识别其中的<script setup> 部分,并将其视为特殊的语法块进行处理。
  • 生成 Setup 函数:Vue 将 <script setup> 部分的代码转换为一个单独的 setup 函数。这个函数中包含了组件的状态(通过 refreactive 等函数定义的响应式数据)、计算属性、事件处理函数等。
  • PropsContext 注入:<script setup> 中可以直接使用 props 和 context,而不需要显式地声明。Vue 在内部会自动将 propscontext 注入到 setup 函数中,使开发者可以直接在其中使用这些变量。
  • 编译优化:使用 <script setup> 语法可以帮助 Vue 进行更好的编译优化。由于所有的组件选项都被包装在一个 setup 函数中,Vue 可以更轻松地进行静态分析和优化,减少运行时的开销。
  • 模块化引入:<script setup> 允许开发者在组件内部直接引入外部模块,而不需要像以前那样将其导入到组件的 <script> 标签中。这简化了组件的导入和使用。
编译过程

当编译器识别到 vue 文件中<script setup>块时,它会将其中的代码视为特殊的语法块,会采取特定的步骤进行处理,如下

  • 单文件组件解析:
    编译器首先会解析整个单文件组件(Single File Component,SFC),包括 <template><script><style> 部分。
    它会逐行扫描文件,查找特定的标记(例如 <script setup>)以确定每个部分的起始和结束。
    一旦找到了 <script setup> 标记,编译器就会开始处理其中的内容。
  • 生成 Setup 函数:

编译器会将 <script setup> 部分的代码转换为一个单独的 setup 函数。这个函数包含了组件的状态、计算属性、事件处理函数等逻辑。
如果在 <script setup> 中使用了 propscontext,编译器会自动将它们注入到 setup 函数中,使开发者可以直接在其中使用这些变量。
如果需要,编译器还会对代码进行必要的转换和优化,以提高性能和可读性。

  • 分析变量依赖关系:

编译器会分析 setup 函数中使用的响应式数据、计算属性和其他变量的依赖关系。
这样可以帮助编译器在组件更新时自动跟踪哪些部分需要重新渲染,从而实现更高效的渲染机制。

  • 生成组件代码:

一旦 setup 函数和其相关的依赖关系被确定,编译器就会将它们与模板部分和其他组件选项(如 propsmethods 等)整合起来,生成最终的组件代码。
这个过程可能会包括将模板编译成渲染函数、合并响应式数据、生成组件的生命周期钩子等步骤。

从中可以看出<script setup> 这种方式比直接使用setup()函数,从性能方面上来说要弱。

@vue/compiler-scf是如何和项目产生联系

我们知道浏览器的引起是无法直接解析.vue文件,因此需要将.vue文件进行编译。vue3 的源码中有一个编译模块@vue/compiler-sfc专门用来编译.vue文件。
那么,@vue/compiler-sfc是如何编译的呢?这可能就取决于我们采用怎样的脚手架,这个模块可以单独的被第三方插件或平台引用

vite中的@vue/compiler-sfc
vite.config.js@vitejs/plugin-vue

基于vite脚手架创建的 vue3 项目时,会安装@vitejs/plugin-vue插件。在vite的默认配置文件中,会引用该插件,代码如下所示:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()], //vue就是@vitejs/plugin-vue中的默认导出 vuePlugin
});

当在终端运行vite命令时,操作会被vite/dist/node/cli捕获,进而调用createServer函数,这个函数是在vite/dist/node/chunks中定义,其返回了一个async 函数,如下所示

// node_modules\vite\dist\node\chunks\dep-41cf5ffd.js
async function _createServer(inlineConfig = {}, options) {
  const config = await resolveConfig(inlineConfig, "serve");
  // 省略
}

配置文件其实就是在resolveConfig中通过loadConfigFromFile方法加载的,默认的配置文件如下所示,vite会去通过参数遍历这些文件名从而实现配置文件的加载。

const DEFAULT_CONFIG_FILES = [
  "vite.config.js",
  "vite.config.mjs",
  "vite.config.ts",
  "vite.config.cjs",
  "vite.config.mts",
  "vite.config.cts",
];

至此@vitejs/plugin-vuevite及其配置就可以做些事情了。

@vitejs/plugin-vue@vue/compiler-sfc

plugin-vue中定义了一个函数vuePlugin,并默认导出,上述vite.config.js中有提及。vuePlugin中定义了一个函数buildStart, vite成功拿到配置文件后会调用buildStart方法, 该方法中又调用了resolveCompiler,如下

   buildStart() {
     const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root);
     if (compiler.invalidateTypeCache) {
       options.value.devServer?.watcher.on("unlink", (file) => {
         compiler.invalidateTypeCache(file);
       });
     }
   },

resolveCompiler中会根据 root 参数目录判断读取node_module中已经安装的 vuepackjson.json,判断版本,如果当前主版本大于 3,则加载vue/compiler-sfc

webpack中的@vue/compiler-sfc
加载项目配置文件

使用vue-cli脚手架创建的 vue3 项目是内置 webpack 的,下面来分析这类项目是如何加载@vue/compiler-sfcvue.config.js是默认的配置文件。当使用cli命令时,会执行如下 js

// node_modules\@vue\cli-service\webpack.config.js
let service = process.VUE_CLI_SERVICE;

if (!service || process.env.VUE_CLI_API_MODE) {
  const Service = require("./lib/Service");
  service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd());
  service.init(process.env.VUE_CLI_MODE || process.env.NODE_ENV);
}

module.exports = service.resolveWebpackConfig();

首次运行时,会调用 service 中的init函数,这个函数会调用loadUserOptions函数,如下所示

loadUserOptions () {
   const { fileConfig, fileConfigPath } = loadFileConfig(this.context)
   console.log("🚀 ~ Service ~ loadUserOptions ~ fileConfig, fileConfigPath:", fileConfig, fileConfigPath)

   if (isPromise(fileConfig)) {
     return fileConfig
       .then(mod => mod.default)
       .then(loadedConfig => resolveUserConfig({
         inlineOptions: this.inlineOptions,
         pkgConfig: this.pkg.vue,
         fileConfig: loadedConfig,
         fileConfigPath
       }))
   }

   return resolveUserConfig({
     inlineOptions: this.inlineOptions,
     pkgConfig: this.pkg.vue,
     fileConfig,
     fileConfigPath
   })
 }

loadFileConfig 函数中默认会读取项目中的vue.config.js

加载vue-loader插件

vue-cli中是通过vue-loader加载@vue/compiler-sfc,而vue-loader插件是在Service类的constructor中加载,constructor会调用resolvePlugins函数,该函数会返回需要加载的插件配置集合plugins,在拿到项目配置文件后,会遍历plugins并调用其apply方法(经过包装后的),以项目配置选项为参数,而plugins中就有这么一个插件./config/base

该文件中调用了vue/cli-shared-utilsloadModule方法来判断当前vue的版本,根据版本的不同调用不同版本的vue-loader, 而针对 vue3 就是在vue-loader/dist/complier中通过require的方式加载编译器vue/compiler-sfc

编译后结果

我们可以引入并打印最初的两个 demo,其中 TheWelcomeComposition API script setup简洁方式,如下

<script setup>
import { onMounted, ref } from "vue";
import TheWelcome from "./components/TheWelcome.vue";
import Welcome from "./components/Welcome.vue";

const TheWelcomeRef = ref();
const WelcomeRef = ref();

onMounted(() => {
  console.log("TheWelcome", TheWelcomeRef.value);
  console.log("Welcome", WelcomeRef.value);
});
</script>

<template>
  <main>
    <TheWelcome ref="TheWelcomeRef"></TheWelcome>
    <Welcome ref="WelcomeRef" />
  </main>
</template>

<style scoped></style>

控制台显示如下图
在这里插入图片描述

二者的值区别很大,通过安装 viteplugin-inspect插件查看它们编译后的区别,setup函数的编译如下

import { ref } from "vue";
const _sfc_main = {
  setup() {
    const count = ref(0);
    const handleClick = () => {
      count.value++;
    };
    return { count, handleClick };
  },
};

<script setup/>的编译结果如下:

import { ref } from "vue";

const _sfc_main = {
  __name: "TheWelcome",
  setup(__props, { expose: __expose }) {
    __expose();

    const count = ref(0);

    const handleClick = () => {
      count.value++;
    };

    const __returned__ = { count, handleClick, ref };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
};

它们都编译生成了_sfc_main对象,其中都定义了setup函数,不同的是<script setup/>方式会默认调用expose函数

  • 11
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jinuss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值