Vue3源码解读(1)-环境搭建

586 阅读5分钟

春节期间在家闲来无事,于是动动小手阅读了 Vue3 的源码,并总结为文章,也是对自己的学习的总结,有任何纰漏欢迎指正~

tip: 本文阅读的 Vue3 源码对于的 commitid 为 4fe4de0

如何上手

首先需要搭建好调试阅读源码的环境。边阅读本文边对照 vue-next 源码是一个比较有效的阅读方法。

方法一

方法一适合阅读整个 Vue 项目的运行流程,包括 compiler 的编译过程和 runtime 的运行流程。

  1. clone Vue3 的源码
git clone git@github.com:vuejs/vue-next.git
  1. 调试 demo

安装完 Vue3 的依赖后,运行 yarn dev , 然后在源码中需要的地方打 debugger,使用 vs code 的插件 live server 打开 packages/vue/examples/composition/commits.html ,打开 chrome 的 devtools,刷新后会命中断点。

tips: yarn dev 相当于运行:

npx rollup -wc --environment "COMMIT:4fe4de0,TARGET:vue,FORMATS:global,SOURCE_MAP:true"

因此我们也可以运行:

npx -n inspect rollup -wc --environment "COMMIT:4fe4de0,TARGET:vue,FORMATS:global,SOURCE_MAP:true"

来调试 Vue3 构建源码的过程。

其中 4fe4de0 是当前 Vue3 仓的 commitId,我们可以运行上面的 rollup 命令来查看 Vue3 源码的构建过程。

packages/vue/examples/composition/commits.html

<script src="../../dist/vue.global.js"></script>
<!-- 
vue.global.js 是 rollup 以 packages/vue/src/index.ts 为入口文件构建的,若要查看 rollup 的构建过程,可以运行:
npx -n inspect rollup -wc --environment "COMMIT:4fe4de0,TARGET:vue,FORMATS:global,SOURCE_MAP:true"
-->
<div id="demo">
  <h1>Latest Vue.js Commits</h1>
  <template v-for="branch in branches">
    <input type="radio" :id="branch" :value="branch" name="branch" v-model="currentBranch">
    <label :for="branch">{{ branch }}</label>
  </template>
  <p>vuejs/vue@{{ currentBranch }}</p>
  <ul>
    <li v-for="{ html_url, sha, author, commit } in commits">
      <a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
      - <span class="message">{{ truncate(commit.message) }}</span><br>
      by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
      at <span class="date">{{ formatDate(commit.author.date) }}</span>
    </li>
  </ul>
</div>

<script>
  const { createApp, ref, watchEffect } = Vue
  const API_URL = `https://api.github.com/repos/vuejs/vue-next/commits?per_page=3&sha=`

  // 截断、删除
  const truncate = v => {
    const newline = v.indexOf('\n')
    return newline > 0 ? v.slice(0, newline) : v
  }

  const formatDate = v => v.replace(/T|Z/g, ' ')

  // createApp 调用的是 runtime-dom/src/index.ts#createApp ,其封装了 packages/runtime-core/src/apiCreateApp.ts#createApp
  const app =
    createApp({
      setup() {
        const currentBranch = ref('master')
        const commits = ref(null)

        watchEffect(() => {
          fetch(`${API_URL}${currentBranch.value}`)
            .then(res => res.json())
            .then(data => {
              console.log(data)
              commits.value = data
            })
        })

        return {
          branches: ['master', 'sync'],
          currentBranch,
          commits,
          truncate,
          formatDate
        }
      }
    });
    debugger

  // app.mount 调用的是 runtime-dom/src/index.ts#mount ,其封装了 packages/runtime-core/src/apiCreateApp.ts#mount
  app.mount('#demo')
</script>

<style>
  #demo {
    font-family: 'Helvetica', Arial, sans-serif;
  }

  a {
    text-decoration: none;
    color: #f66;
  }

  li {
    line-height: 1.5em;
    margin-bottom: 20px;
  }

  .author,
  .date {
    font-weight: bold;
  }
</style>

方法二

方法二适合调试阅读项目具体的某个方法或某块代码。

安装 vscode 插件 jest runner,打开 Vue3 的测试文件,然后在相应的位置打断点,点击 debug 字样即可开始调试。

aaaaa.png

有时候会碰到缺少依赖包导致运行失败的情况,安装相应的 npm 包即可。

项目结构

  1. Vue3 的项目结构:

Vue3 的主要模块位于 packages 目录,我们 cd 到 packages 运行 tree -aI ".git*|.vscode" -C -L 1 列出所有的模块。

packages
├── compiler-core 平台无关的编译器. 它既包含可扩展的基础功能,也包含所有平台无关的插件。
├── compiler-dom 针对浏览器而写的编译器。
├── compiler-sfc
├── compiler-ssr
├── global.d.ts
├── reactivity 数据响应式系统,这是一个单独的系统,可以与任何框架配合使用
├── runtime-core 与平台无关的运行时。其实现的功能有虚拟 DOM 渲染器、Vue 组件和 Vue 的各种API,我们可以利用这个 runtime 实现针对某个具体平台的高阶 runtime,比如自定义渲染器。
├── runtime-dom 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。
├── runtime-test 一个专门为了测试而写的轻量级 runtime。由于这个 rumtime 「渲染」出的 DOM 树其实是一个 JS 对象,所以这个 runtime 可以用在所有 JS 环境里。你可以用它来测试渲染是否正确。它还可以用于序列化 DOM、触发 DOM 事件,以及记录某次更新中的 DOM 操作。
├── server-renderer 用于 SSR
├── sfc-playground
├── shared 没有暴露任何 API,主要包含了一些平台无关的内部帮助方法。
├── size-check
├── template-explorer
└── vue 用于构建「完整构建」版本,引用了上面提到的 runtime 和 compiler。

其中最核心的模块可以分为两大部分:编译系统(compiler)和运行时(runtime,包括 reactivity),模块间的继承关系如下:

                                    +---------------------+
                                    |                     |
                                    |  @vue/compiler-sfc  |
                                    |                     |
                                    +-----+--------+------+
                                          |        |
                                          v        v
                      +---------------------+    +----------------------+
                      |                     |    |                      |
        +------------>|  @vue/compiler-dom  +--->|  @vue/compiler-core  |
        |             |                     |    |                      |
   +----+----+        +---------------------+    +----------------------+
   |         |
   |   vue   |
   |         |
   +----+----+        +---------------------+    +----------------------+    +-------------------+
        |             |                     |    |                      |    |                   |
        +------------>|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
                      |                     |    |                      |    |                   |
                      +---------------------+    +----------------------+    +-------------------+
  1. Vue3源码的构建产物

Vue3 源码的构建产物位于packages/vue/dist目录,包含:

├── vue.cjs.js                   适用于 commonjs 方式引入的 vue3 包,包括编译时compile和运行时runtime
├── vue.cjs.js.map
├── vue.cjs.prod.js              生产模式的包
├── vue.cjs.prod.js.map
├── vue.esm-browser.js           适用于 esmodule 方式引入的 vue3 包,适用于浏览器环境,包括编译时和运行时
├── vue.esm-browser.js.map
├── vue.esm-browser.prod.js
├── vue.esm-browser.prod.js.map
├── vue.esm-bundler.js           适用于 esmodule 方式引入的 vue3 包,适用于node环境,包括编译时和运行时
├── vue.esm-bundler.js.map
├── vue.global.js                既适用于浏览器环境又适用于node环境的包
├── vue.global.js.map
├── vue.global.prod.js
├── vue.global.prod.js.map
├── vue.runtime.esm-browser.js   只包含运行时的包
├── vue.runtime.esm-browser.js.map
├── vue.runtime.esm-browser.prod.js
├── vue.runtime.esm-browser.prod.js.map
├── vue.runtime.esm-bundler.js
├── vue.runtime.esm-bundler.js.map
├── vue.runtime.global.js
├── vue.runtime.global.js.map
├── vue.runtime.global.prod.js
└── vue.runtime.global.prod.js.map

阅读建议

  1. 如果你只需要了解 Vue3 最核心的特性,可以直接看编译系统或响应式系统的部分。
  2. 如果你想了解一个 Vue 应用从初始化到挂载、diff、卸载的整个过程,可以前往后阅读本系列文章。
  3. 如果你工作中不需要了解 Vue3 运行工程中的某些细节,直接跳过即可,一般不会影响你阅读后面的内容。

liftcycle.png

运行时(Runtime)+ 编译器(Compiler) vs. 只包含运行时(Runtime-only)

如果你需要动态编译模版(比如:将字符串模版传递给 template 选项,或者通过提供一个挂载元素的方式编写 html 模版),你将需要编译器,因此需要一个完整的构建包。

当你使用 vue-loader 或者 vueify 时,*.vue 文件中的模版在构建时会被编译为 JavaScript 的渲染函数。因此你不需要包含编译器的全量包,只需使用只包含运行时的包即可。

对比 vue.global.jsvue.runtime.global.js 可知,371581 / 579987 = 0.64,只包含运行时的包体积仅为全量包的体积的 64%,因此我们应该尽量使用只包含运行时的包。

各个包的大小, 单位 bit:

vue.cjs.js                              2875              
vue.cjs.js.map                          5196              
vue.cjs.prod.js                         2268              
vue.cjs.prod.js.map                     4027              
vue.esm-browser.js                      530764              
vue.esm-browser.js.map                  1111379              
vue.esm-browser.prod.js                 112250              
vue.esm-browser.prod.js.map             874911              
vue.esm-bundler.js                      2828              
vue.esm-bundler.js.map                  5140              
vue.global.js                           579987              
vue.global.js.map                       1124410              
vue.global.prod.js                      109903              
vue.global.prod.js.map                  868571              
vue.runtime.esm-browser.js              350861              
vue.runtime.esm-browser.js.map          725244              
vue.runtime.esm-browser.prod.js         72336              
vue.runtime.esm-browser.prod.js.map     596109              
vue.runtime.esm-bundler.js              875              
vue.runtime.esm-bundler.js.map          1797              
vue.runtime.global.js                   371581              
vue.runtime.global.js.map               724952              
vue.runtime.global.prod.js              71782              
vue.runtime.global.prod.js.map          589807

我们会按照代码的执行顺序,包括 Vue 应用的初始化、挂载、模板编译、patch 等过程,完整的学习一下 Vue3 应用从初始化到渲染完成的整个过程。

Vue3 源码解读

参考文献:

  1. juejin.cn/post/694937…
  2. juejin.cn/post/684490…
  3. vue3js.cn/global/