【vue源码01】从目录结构入手Vue3源码😎
今天开始不再借助源码解读书籍和博客,真真切切的自己下载Vue3源码来研究啦😎
# 源码下载
很简单,直接从github上clone下来就好了
git clone https://github.com/vuejs/vue-next.git
下载到源码后用VSCode打开,就可以看到如下的项目结构了
# 项目结构
拿到源码后,当然要首先分析一下项目结构啦。最先关注packages目录,核心的代码都在packages目录下,里面各文件的功能是这样的:
- /compiler-core
Vue 3 编译器的核心部分,负责将模板转换为渲染函数。主要功能包括解析(parsing)、转换(transforming)和代码生成(code generation)。它不包含与平台相关的逻辑,因此是平台无关的。
- /compiler-dom
针对浏览器的编译器扩展,依赖于 compiler-core。它添加了与浏览器相关的优化和代码生成逻辑,适用于将 Vue 模板编译为浏览器可以执行的代码。
- /compiler-sfc
负责处理单文件组件(SFC,Single File Component)。它解析 .vue 文件,将其中的 template、script 和 style 标签分离并处理,生成可用于编译和渲染的代码。
- /compiler-ssr
服务器端渲染(SSR)的编译器扩展,依赖于 compiler-core。它提供了针对服务器端渲染的优化和代码生成逻辑,使 Vue 应用可以在服务器端预渲染。
- /runtime-core
Vue 3 的核心运行时库,包含响应式系统、虚拟 DOM、组件系统等核心功能。它是平台无关的,可以与不同的渲染器(如 DOM 渲染器、原生渲染器)配合使用。
- /runtime-dom
是针对浏览器的运行时库,依赖于 runtime-core。它提供了与浏览器 DOM 相关的 API 和功能,使得 Vue 可以在浏览器环境中运行。
- /runtime-test
提供了一个用于测试的运行时实现。它通常用于 Vue 的单元测试和集成测试,模拟了 DOM 操作和组件行为,方便测试 Vue 应用的逻辑。
- /server-renderer
包含用于服务器端渲染的实现。它利用 compiler-ssr 生成的代码,在服务器端将 Vue 组件渲染为 HTML 字符串,从而支持服务器端渲染(SSR)。
- /shared
包含多个包之间共享的工具函数和类型定义。它包括通用的工具函数、类型声明和辅助函数,供其他包使用,避免代码重复。
- /dts-built-test
包含自动生成的 TypeScript 声明文件的测试用例。用于确保生成的类型声明文件正确且完整。
- /dts-test
是手动编写的 TypeScript 声明文件的测试用例。用于验证 TypeScript 类型在不同使用场景下的正确性。
- /reactivity
是 Vue 3 响应式系统的实现。它提供了响应式对象、计算属性和观察者(watcher)等核心功能,是 Vue 3 响应式数据流的基础。
- /sfc-playground
是一个在线编辑和运行单文件组件(SFC)的工具,通常用于演示和测试。它允许用户在浏览器中编写和预览 .vue 文件。
- /template-explorer
是一个用于调试和测试模板编译的工具。它允许开发者输入 Vue 模板并查看生成的渲染函数和编译输出。
- /vue
包含 Vue 3 的核心入口和整合包。它将各个核心模块和运行时整合在一起,提供给最终用户使用。这个包是用户安装 Vue 3 时所获取的主要包。
- /vue-compat
是一个兼容层,提供与 Vue 2 兼容的 API 和行为。它帮助从 Vue 2 迁移到 Vue 3 的项目在迁移过程中保持兼容性,逐步适应 Vue 3 的新特性和变化。
其中,最核心的包就是
- /reactivity:负责构建响应式系统
- /runtime-core:运行时核心,包含虚拟DOM、渲染器、组件系统等核心逻辑
- /compiler-core:将模板渲编译成渲染函数
也就是我们常说的编译器、渲染器和响应式系统啦
# 编译的入口
如此庞大的一个工程,他的各个模块之间是怎么相互协调、互相依赖的呢?在这之前,我认为还是需要先搞懂这个项目是怎么跑起来的,也就是找到入口。
拿到一个项目,可能很多人都会束手无策,不知道从哪里开始切入阅读,虽然我阅读的源码也不多,但是我觉得package.json是一个非常不错的切入点。
package.json文件是npm包管理器中一个重要的配置文件,它定义了包的名称、版本、依赖关系、脚本命令等,从package.json里面可以俯瞰整个项目的结构和依赖,让我们对项目有一个大概的认识。
比如定义了哪些指令:
// package.json
"script": {
// ......
"dev": "node scripts/dev.js",
"build": "node scripts/build.js",
// ......
}
项目用到了哪些库:
// package.json
"devDependencies": {
// ...
"@babel/parser": "^7.24.5",
// ...
"rollup": "^4.17.2",
// ...
"typescript": "~5.4.5",
"vite": "^5.2.11",
}
这样就可以对项目有个大概的认知
那回到最初的问题,vue项目是怎么跑起来的呢?我们在运行一个项目的时候,都会执行npm run dev指令,而要打包部署到生产环境是则会执行npm run build指令,vue当然也是这样,所以我们的切入点就是这两个指令
就看npm run dev指令吧
"dev": "node scripts/dev.js"
我们执行npm run dev时,实际执行的指令是后面的 node scripts/dev.js,也就是scripts/dev.js这个文件,进去看看
// import各种模块
// ...
// 初始化一些参数
const args = minimist(process.argv.slice(2))
const targets = args._.length ? args._ : ['vue']
// ...
for (const target of targets) {
// ...
}
文件里面大概就这样吧,整个文件干了这些工作:
- 导入模块,如esbuild、node内置模块、minimist解析命令行参数等
- 初始化变量,如要读取和操作的文件系统、从命令行中解析的模板和格式、构建的模板和输出格式等
- 确定输出格式
- 处理内部依赖
- 定义插件
- 配置esbuild
- 开始构建并监听,实现热更新
这里我用了阿里的通义灵码来读,这是阿里的Ai产品,还挺好用的,安利一下~
先看参数部分吧
import esbuild from 'esbuild'
import { dirname, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import minimist from 'minimist'
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
const require = createRequire(import.meta.url)
const __dirname = dirname(fileURLToPath(import.meta.url))
const args = minimist(process.argv.slice(2))
const targets = args._.length ? args._ : ['vue']
const format = args.f || 'global'
const prod = args.p || false
const inlineDeps = args.i || args.inline
这里的参数看起来很多,但总结起来也很简单,大概就是通过minimist解析命令行里面带的参数,然后把参数保存到变量中,方便后续使用
比如命令行中执行这样的命令:
node script.js --format=cjs --devOnly --target package1 package2
则args对象解析出来后就是这样的:
{
format: 'cjs',
devOnly: true,
target: ['package1', 'package2'],
_: []
}
所以我们正常npm run dev时,其他参数基本都是没有的,而编译目标target只有一个值vue,我们就先关注target吧
看看最核心的处理逻辑
for (const target of targets) {
const pkg = require(`../packages/${target}/package.json`)
// ...
// 这里执行了一些参数处理,比如指定包、指定输出目录、编译的条件等待,就先不关注了
// ...
esbuild
.context({
entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
outfile,
bundle: true,
external,
sourcemap: true,
format: outputFormat,
globalName: pkg.buildOptions?.name,
platform: format === 'cjs' ? 'node' : 'browser',
plugins,
define: {
__COMMIT__: `"dev"`,
__VERSION__: `"${pkg.version}"`,
__DEV__: prod ? `false` : `true`,
__TEST__: `false`,
__BROWSER__: String(
format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches,
),
__GLOBAL__: String(format === 'global'),
__ESM_BUNDLER__: String(format.includes('esm-bundler')),
__ESM_BROWSER__: String(format.includes('esm-browser')),
__CJS__: String(format === 'cjs'),
__SSR__: String(format === 'cjs' || format.includes('esm-bundler')),
__COMPAT__: String(target === 'vue-compat'),
__FEATURE_SUSPENSE__: `true`,
__FEATURE_OPTIONS_API__: `true`,
__FEATURE_PROD_DEVTOOLS__: `false`,
__FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: `false`,
},
})
.then(ctx => ctx.watch())
}
省去了一些处理输出路径、插件等参数的逻辑,其实就是遍历target中的每个编译目标,然后分别用esbuild进行编译,再.then中监听更改罢了
因为执行的是最简单的npm run dev指令,targets里面只有一项vue,循环也只会执行一次
可以从esbuild.content传入的配置对象中看到编译的路口:
entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)]
也就是packages/vue/src/index.ts这个文件啦,刚好对应我们之前解释packages中vue这个目录的作用
/vue
包含 Vue 3 的核心入口和整合包。它将各个核心模块和运行时整合在一起,提供给最终用户使用。这个包是用户安装 Vue 3 时所获取的主要包。
# 包与包之间的依赖关系
上面我们已经找到项目的入口文件了,接下来就可以一边调试,一边慢慢读源码了。回到之前的问题,包与包之间的依赖关系要怎么看呢?搞清楚这些包的依赖关系,可以让我们在读源码的时候更加顺畅
vue的工程项目是用pnpm包管理器进行管理的,也就是在pnpm-lock.yaml中我们可以看到各包之间的依赖关系,例如:
# ...
packages/compiler-core:
dependencies:
'@babel/parser':
specifier: ^7.24.5
version: 7.24.5
'@vue/shared':
specifier: workspace:*
version: link:../shared
entities:
specifier: ^4.5.0
version: 4.5.0
estree-walker:
specifier: ^2.0.2
version: 2.0.2
source-map-js:
specifier: ^1.2.0
version: 1.2.0
devDependencies:
'@babel/types':
specifier: ^7.24.5
version: 7.24.5
packages/compiler-dom:
dependencies:
'@vue/compiler-core':
specifier: workspace:*
version: link:../compiler-core
'@vue/shared':
specifier: workspace:*
version: link:../shared
# ...
packages中的相关内容全部丢给GPT,让他帮我整理一下:
- shared 是多个包的共同依赖。
- compiler-core 依赖 shared。
- compiler-dom 依赖 compiler-core 和 shared。
- compiler-sfc 依赖 compiler-core, compiler-dom, compiler-ssr 和 shared。
- compiler-ssr 依赖 compiler-dom 和 shared。
- dts-built-test 依赖 reactivity, shared 和 vue。
- dts-test 依赖 dts-built-test 和 vue。
- reactivity 依赖 shared。
- runtime-core 依赖 reactivity 和 shared。
- runtime-dom 依赖 runtime-core 和 shared。
- runtime-test 依赖 runtime-core 和 shared。
- server-renderer 依赖 compiler-ssr, shared 和 vue。
- vue 依赖 compiler-dom, compiler-sfc, runtime-dom, server-renderer 和 shared。
- vue-compat 依赖 vue。
ai真方便🤣