vue3源码解析—前言

730 阅读2分钟

全面拥抱 Vue 3

在 2022 年 2 月 7 日,Vue 3 成为vue的默认版本。Vue 2.7 成为 Vue 2.x 的最终次要版本。可以预见:在不久的将来,Vue 2.x 将会逐步淡出江湖,Vue 3 将会成为代替 Vue 2 的主流框架。 Vue 3 整体的设计相对于 Vue 2.x 有着不小的变化,我们先着重介绍几个相对于 Vue 2.x 而言较大的几个变化,这里的介绍不会深入到源码的细节。

1. 源码组织上的变化

Vue 3 相对于 Vue 2 使用 monorepo 的方式进行包管理,使得 Vue 3 源码模块职责显得特别地清晰明了,每个包独立负责一块核心功能的实现,方便开发和测试。

2. 引入 Composition API

相对而言,Composition API 更适用于大型的项目,因为大型项目可能会产生大量状态逻辑的维护,甚至跨组件的逻辑复用;而对于中小型项目来说,Options API 可以在你写代码时减少思考组织状态逻辑的方式,也是一种不错的选择。

3. 运作机制的变化

vue 2 的运行机制如下: vue2 运行机制 大致流程为:

  1. 先通过 new Vue() 创建组件实例;
  2. 然后在创建组件实例过程中进行initStateoptions合并、initLifecycleinitRender$mount
  3. 并且将模板转化成render函数,最后执行render函数,进行依赖收集;
  4. 当依赖发生改变时,触发相关的watcher重新执行。

Vue 3 则在底层实现中,摒弃了 Vue 2 的部分实现,采用全新的响应式模型进行重写。vue 3 的运行机制如下: vue3运行机制 可以看到 vue 3 的几个重要变化

  • 首先,之前通过 new Vue() 来创建 Vue 对象的方式已经变成了 createApp
  • 其次,在响应式部分也由原来的 Object.defineProperty 改成了现在的 Proxy API 实现
  • 另外,针对响应式依赖收集的内容,在 Vue 2.x 版本中是收集了 Watcher,而到了 Vue 3 中则变成了 effect

4. 其他方面的变化

除了上面所说的这些变化外,Vue 3 不管是在编译时、还是在运行时都做了大量的性能优化。例如,在编译时,Vue 3 通过标记 /*#__PURE__*/ 来为打包工具提供良好的 Tree-Shaking 机制,通过 静态提升 机制,避免了大量静态节点的重复渲染执行在运行时,又通过批量队列更新机制优化了更新性能,通过 PatchFlagsdynamicChildren 进行了 diff 的靶向更新……

环境搭建

  1. 创建文件夹 mini-vue3-zf-pnpm,并初始化项目
    pnpm init
    
  2. 创建 packages/reactivity/src/index.tspackages/shared/src/index.ts 两个文件夹
  3. 创建 .npmrc 配置文件
    # pnpm安装vue时,默认不会将vue依赖的包展平到 node_modules 下,导致项目中无法使用vue的依赖
    # 通过以下配置将 vue 依赖的包展平到 node_modules 下
    shamefully-hoist = true
    
  4. 创建 pnpm-workspace.yaml,定义工作空间的根目录
    packages:
     - "packages/*"
    
  5. 安装 esbuildtypescriptminimist minimist 是一个专门用于处理Node.js启动参数的库)
    pnpm install esbuild typescript minimist -D -w
    
  6. 初始化 typescript 配置文件
    pnpm tsc --init
    
     {
         "compilerOptions": {
             "outDir": "dist", // 输出的目录
             "sourceMap": true,  // 采用 sourcemap
             "target": "es2016", // 目标语法
             "module": "esnext", // 模块格式
             "moduleResolution": "node", // 解析模块的方式
             "strict": false,  // 严格模式
             "resolveJsonModule": true,  // 解析json模块
             "esModuleInterop": true,  // 允许通过es6语法引入 commonjs 模块
             "jsx": "preserve",  // jsx 不转义
             "lib": ["esnext", "dom"]  // 支持的类库:esnext和dom
         }
     }
    
  7. 进入 reactivity 模块,初始化子项目
    cd packages
    cd reactivity
    pnpm init
    
    修改生成的 package.json
     {
         "name": "@vue/reactivity",
         "version": "1.0.0",
         "description": "",
         "module": "dist/reactivity.esm-bundler.js",
         "unpkg": "dist/reactivity.global.js",
         "buildOptions": {
             "name": "VueReactivity",
             "formats": [
                 "esm-browser",
                 "esm-bundler",
                 "cjs",
                 "global"
             ]
         }
     }
    

    formats 为自定义的需要打包的格式:

    • global:立即执行函数的格式,会暴露全局对象
    • esm-browser:在浏览器中使用的格式,内联所有的依赖项
    • esm-bundler:在构建工具中使用的格式,不提供 .prod 格式,在构建应用程序时会被构建工具一起进行打包压缩
    • cjs:在 node 中使用的格式,例如服务端渲染
  8. 同理,也进入 shared 模块,修改生成的 package.json
     {
         "name": "@vue/shared",
         "version": "1.0.0",
         "module": "dist/shared.esm-bundler.js",
         "buildOptions": {
             "formats": [
                 "esm-bundler",
                 "cjs"
             ]
         }
     }
    
  9. reactivity 模块安装 shared 模块的依赖:
    pnpm install @vue/shared@workspace --filter @vue/reactivity
    
  10. tsconfig.json 中配置路径别名:
    "baseUrl": ".",
    "paths": {
      "@vue/*": ["packages/*/src"]
    }
    
  11. 对模块进行打包
    • 在根package.json中添加命令:"dev": "node scripts/dev.js reactivity -f esm"
    • 创建 scripts/dev.js
      const { build } = require("esbuild");
      const path = require("path");
      const args = require("minimist")(process.argv.slice(2)); // { _: [ 'reactivity' ], f: 'esm' }
      
      // 打包的模块是哪个
      const target = args._[0] || "reactivity";
      // 打包的格式是什么
      const format = args.f || "global";
      // 读取模块的 package.json 文件
      const pkg = require(path.resolve(
          __dirname,
          `../packages/${target}/package.json`
      ));
      // 输出格式:把 global 改成 iife
      const outputFormat = format.startsWith("global")
          ? "iife"
          : format === "cjs"
          ? "cjs"
          : "esm";
      // 输出的路径
      const outfile = path.resolve(
          __dirname,
          `../packages/${target}/dist/${target}.${format}.js`
      );
      
      // 使用esbuild打包
      build({
          entryPoints: [path.resolve(__dirname, `../packages/${target}/src/index.ts`)], // 入口
          outfile, // 出口
          bundle: true, // 是否打包到一起
          sourcemap: true, // 是否生成sourcemap文件(.map 结尾)
          format: outputFormat, // 打包的格式
          globalName: pkg.buildOptions?.name, // 打包的全局名称
          platform: format === "cjs" ? "node" : "browser", // 平台
          watch: {
              // 监控文件变化
              onRebuild(error) {
              if (!error) {
                  console.log("rebuild~~~");
              }
              },
          },
      }).then(() => {
          console.log("watching~~~");
      });
      

接下来,只需要执行 pnpm run dev 即可对 reactivity 模块打包,并在 reactivity/dist 目录下生成打包好的文件。

总结

本小节,我们阐述了 vue 3 相较于 vue 2 的几个重大改变,并从零搭建了 mini-vue3 的开发环境。最后,让我们一起进入 Vue 3 的世界,探索其中的奥秘吧!