Jinuss's blog Jinuss's blog
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

东流

前端可视化
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • React

  • Vue

    • vue-demi介绍
    • script标签的setup实现原理
      • 前端中的拖拽知识
      • rollup-plugin-visualizer
    • JavaScript文章

    • 学习笔记

    • openlayers

    • threejs

    • MapboxGL

    • 工具

    • 源码合集

    • 前端
    • Vue
    东流
    2024-05-31
    目录

    script标签的setup实现原理

    # 概述

    当 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>
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
    • 组合式

    <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>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    很明显,第二种方式 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 函数。这个函数中包含了组件的状态(通过 ref、reactive 等函数定义的响应式数据)、计算属性、事件处理函数等。
    • Props 和 Context 注入:<script setup> 中可以直接使用 props 和 context,而不需要显式地声明。Vue 在内部会自动将 props 和 context 注入到 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> 中使用了 props 和 context,编译器会自动将它们注入到 setup 函数中,使开发者可以直接在其中使用这些变量。 如果需要,编译器还会对代码进行必要的转换和优化,以提高性能和可读性。

    • 分析变量依赖关系:

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

    • 生成组件代码:

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

    从中可以看出<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
    });
    
    1
    2
    3
    4
    5
    6

    当在终端运行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");
      // 省略
    }
    
    1
    2
    3
    4
    5

    配置文件其实就是在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",
    ];
    
    1
    2
    3
    4
    5
    6
    7
    8

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

    # @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);
           });
         }
       },
    
    1
    2
    3
    4
    5
    6
    7
    8

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

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

    使用vue-cli脚手架创建的 vue3 项目是内置 webpack 的,下面来分析这类项目是如何加载@vue/compiler-sfc。vue.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();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    首次运行时,会调用 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
       })
     }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    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-utils的loadModule方法来判断当前vue的版本,根据版本的不同调用不同版本的vue-loader, 而针对 vue3 就是在vue-loader/dist/complier中通过require的方式加载编译器vue/compiler-sfc

    # 编译后结果

    我们可以引入并打印最初的两个 demo,其中 TheWelcome为Composition 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>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    控制台显示如下图

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

    import { ref } from "vue";
    const _sfc_main = {
      setup() {
        const count = ref(0);
        const handleClick = () => {
          count.value++;
        };
        return { count, handleClick };
      },
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    <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__;
      },
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

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

    编辑 (opens new window)
    上次更新: 2025/04/09, 10:15:29
    vue-demi介绍
    前端中的拖拽知识

    ← vue-demi介绍 前端中的拖拽知识→

    最近更新
    01
    GeoJSON
    05-08
    02
    Circle
    04-15
    03
    CircleMarker
    04-15
    更多文章>
    Theme by Vdoing | Copyright © 2024-2025 东流 | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式