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>
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
});
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");
// 省略
}
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",
];
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);
});
}
},
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();
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
})
}
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>
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 };
},
};
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__;
},
};
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
函数