Vue3源码解析 -- 1.源码调试

542 阅读7分钟

基于vue@3.3.4源码

Vue3 源码构建

1、下载 vue3.3.4 源码 https://codeload.github.com/vuejs/core/zip/refs/tags/v3.3.4

2、源码 .github/CONTRIBUTING.md 文件中有描述怎么安装依赖,使用 pnpm i 安装依赖

3、安装依赖的时候,遇到 puppeteer 下载问题,需要在项目根目录添加.npmrc,加上这一行代码

puppeteer-download-base-url="https://cdn.npmmirror.com/binaries/chrome-for-testing"

4、在 package.json 中的 scripts 中添加如下命令 build-vue-only,主要是只打包全局的 vue 和输出 sourcemap 文件。package.json 中默认有 build 命令,是打包所有的 package,并且打包所有格式。

"build-vue-only": "node scripts/build.js vue --formats global --sourcemap true",

5、执行 pnpm build-vue-only,会在 packages/vue/dist 文件夹下生成 vue.global.jsvue.global.js.map,接下来就可以直接使用了。

Vue3 源码目录

在项目目录运行命令 tree -aI ".git*|.vscode|node_modules" -C -L 2,会打印目录结构,我们只分析 packages 下面的目录。

├── packages
│   ├── compiler-core        // 包含Vue编译器的核心功能,提供了编译器的基本逻辑和算法。比如编译成抽象语法树等。
│   ├── compiler-dom         // 包含Vue编译器针对浏览器环境的特定功能,例如处理DOM元素和属性。
│   ├── compiler-sfc         // 包含Vue单文件组件(SFC)编译器,用于将单文件组件转换为渲染函数。
│   ├── compiler-ssr         // 包含Vue服务器端渲染(SSR)编译器,用于将Vue组件编译为在服务器上渲染的函数。
│   ├── dts-test             // 包含用于测试Vue类型声明文件的相关工具和测试用例。
│   ├── global.d.ts          // 全局类型声明文件,用于定义全局变量和类型。
│   ├── reactivity           // 包含Vue响应式系统的实现,用于实现数据的响应式和依赖追踪。
│   ├── reactivity-transform // 包含用于转换Vue 2.x代码以适应Vue 3响应式系统的工具。
│   ├── runtime-core         // 包含Vue运行时的核心功能,提供了Vue实例的创建、组件的渲染等基本功能。
│   ├── runtime-dom          // 包含Vue运行时针对浏览器环境的特定功能,例如处理DOM操作和事件处理。
│   ├── runtime-test         // 包含用于测试Vue运行时的相关工具和测试用例。
│   ├── server-renderer      // 包含Vue服务器端渲染(SSR)的相关功能,用于在服务器上渲染Vue组件。
│   ├── sfc-playground       // 包含用于在浏览器中演示和测试Vue单文件组件的工具。
│   ├── shared               // 包含Vue内部共享的工具函数和常量。
│   ├── size-check           // 包含用于检查Vue库大小的工具和配置文件。
│   ├── template-explorer    // 包含用于在浏览器中探索和分析Vue模板的工具。
│   ├── vue                  // 包含面向用户的Vue的完整版本,包括编译器和运行时。
│   └── vue-compat           // 包含用于向后兼容Vue 2.x的功能和API的适配层。

其中最重要的 5 个模块,分别是:compiler-core,compiler-dom,compiler-sfc,runtime-core,runtime-dom,reactivity。

compile 可以理解为编译器,将 vue 的语法,代码,编译成 JS 的过程。而 runtime 则是程序运行时的一些方法,比如创建实例,生命周期,渲染等 vue 的核心功能。

Vue3 打包分析

项目根目录可以看到 rollup.config.js,vue3 正是使用 rollup 打包的,在这个配置文件中,可以看到下面这行的代码:

let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`;

entryFile 就是打包的入口文件。在 packages 目录下面的每一个文件夹,都可以被单独打包,我们现在只分析 packages/vue 这个包。所以入口文件就是 packages/vue/src/runtime.ts 或者 packages/vue/src/index.ts,具体是哪个入口,取决于你打包时选择的格式,是打包 rumtine 相关的,还是打包整个 Vue。

打包生成的文件

package.json 中默认有 build 命令,是打包所有的 package,并且生成不同格式的文件。分别有 iife,cjs,es 格式。

  • iife 表示立即执行函数,可以直接在 html 中使用;
  • cjs 表示用在 Nodejs 环境,或其他支持 CommonJS 的环境;
  • es 表示生成的文件遵循 ES6 的模块规范。

我们只打包 vue 的话,生成的文件名字分别是 vue.global.jsvue.cjs.jsvue.esm-browser.jsvue.esm-bundler.jsvue.runtime.global.jsvue.runtime.esm-browser.jsvue.runtime.esm-bundler.js

其中大致分为两种,一种是带 runtime 的,表示这个文件是纯 Vue 功能的,包含 Vue 运行时的各种方法,但是不支持模版语法等,也就是不支持编译。另外一种不带 runtime 的,表示是全部的代码,既包含编译部分,也包含 Vue 的运行时功能。

  • cjs:CommonJS 模块格式的文件,适用于 Node.js 环境或其他支持 CommonJS 的环境。
  • global:将 Vue 3 作为一个全局变量暴露给浏览器环境使用。
  • global-runtime:仅包含运行时代码的全局变量模块文件,适用于浏览器环境。相比 global.js 就是少了 compile 方法。
  • esm-bundler 和 esm-browser:这两个配置用于打包 ES 模块格式的文件。esm-bundler 用于打包供 bundler(如 Rollup 或 Webpack)使用,而 esm-browser 用于打包供浏览器环境使用。
  • esm-bundler-runtime 和 esm-browser-runtime:这两个配置用于打包仅包含运行时代码的 ES 模块文件。esm-bundler-runtime 用于 bundler 环境,如 Rollup 或 Webpack,而 esm-browser-runtime 用于浏览器环境。

Vue3 使用和调试

1、在 packages/vue/examples 文件夹下创建 debug 文件夹,并且在 debug 文件夹下创建 index.html

2、将下面的代码复制到文件中

3、浏览器运行 index.html

4、开始打断点调试,从 createApp 方法开始

<script src="../../dist/vue.global.js"></script>

<!-- App组件的模板 -->
<script type="text/x-template" id="app">
  <div @click="add">{{name}}</div>
</script>

<!-- app渲染的根节点 -->
<div id="demo"></div>

<script>
  const { createApp, ref } = Vue;
  // 根组件
  const App = {
    template: '#app',
    setup() {
      const name = ref(0);
      const add = () => {
        name.value++;
      };

      return {
        name,
        add,
      };
    },
  };
  // 创建实例
  const app = createApp(App);
  // 渲染
  app.mount('#demo');
</script>

其他

不能打断点的代码

打包的时候,是有 sourcemap 的,所以引入的 vue.global.js 在调试时会指向真正的未打包之前的源码。但是调试过程发现在源代码上,有的不能打断点,是灰色的,像下图。

debug

这些 __XXX__ 类型的变量通常用于在开发环境和生产环境之间进行条件编译,以实现在开发阶段添加额外的调试信息或者去除一些不必要的代码。这些变量通常是在构建过程中通过构建工具的插件定义的。

vue3 打包时,用到了 rollup-plugin-esbuild 的 define 属性,设置了许多预定义常量。类似的功能还有 webpack.DefinePlugin,@rollup/plugin-replace 等插件。

所以那些灰色的代码,其实是因为在 vue.global.js 中不存在,所以源码调试时,相当于在源码中没有映射到该代码。

存粹的函数或表达式

源码中经常会看到类似于 /*#__PURE__*/ 的注释,这是一种特殊的注释语法,用于告诉工具或编译器某个函数或表达式是纯粹的(pure),没有副作用的,可以安全的被替换或者删除。以便在编译过程中进行优化,或减小最终生成的代码的体积。

源码被打包成 vue.global.js 后,/*#__PURE__*/ 注释会变为 /* @__PURE__ */ 注释,这是 esbuild 做的,将这种注释统一了下。后续 Terser 压缩时可以识别,当然 Terser 中也支持 # 开头的注释。

打包时为什么 rollup 和 esbuild 同时使用

Vue3 在打包时,使用了 rollup-plugin-esbuild 插件,借助了 esbuild 在构建速度方面的强大优势。在 Rollup 构建过程中使用 esbuild 引擎进行快速代码转换,如 JSX 转换、TypeScript 转换等,而无需额外配置和使用 @rollup/plugin-babel 和 @rollup/plugin-typescript 插件。

同时由于 Rollup 的强大的生态,有大量的插件,且在处理模块化代码、代码拆分和库的构建方面有很强的优势。比如分模块打包,打包成不同格式的文件等。

因此,Vue 3 选择在 Rollup 中使用 rollup-plugin-esbuild 插件,以便充分利用 esbuild 引擎的性能优势,同时保留 Rollup 的灵活性和丰富的生态系统。这样可以在保证性能的同时,与现有的 Vue 生态系统和插件进行良好的集成。