Vue3源码的主体结构及初始化流程

824 阅读5分钟

「这是我参与2022首次更文挑战的第36天,活动详情查看:2022首次更文挑战

主体架构

代码组织

  • TypeScript 代码编写
  • rollup 打包
  • jest 单元测试
  • eslint 代码检查
  • prettier 代码格式化

目录结构

  • packages 代码包,里面主要分为以下三个大模块:
    • compiler 编译相关
    • reactivity 响应式
    • runtime 运行时
  • scripts 脚本

上文Vue 源码学习开篇!中我们使用 pnpm dev进行了开发环境打包,并且添加了 --sourcemap 配置,这个命令会执行 scripts/dev.js 文件进行打包,接下来我们看一下打包这个动作 vue 到底都做了什么,然后基于它去理解其他的打包命令就可以了。

scripts:dev.js.png

上图我对 scripts/dev.js 文件进行了逐行注释,大体逻辑是基于命令获取各种打包配置,然后利用 execa 执行打包脚本,并传入打包配置。
读懂了 pnpm dev 的逻辑,其他的打包命令基本就大同小异了。

模块依赖关系

目录结构部分提到 packages 代码包中主要分为以下三个大模块:

  • compiler 编译相关
  • reactivity 响应式
  • runtime 运行时 所以这里我们讲主体的依赖关系就是下图所示: 模块依赖关系.png

上面是一个简单的主体依赖关系图,这里我们大致说一下一个 Vue 页面的运行,解释一下这三个模块是如何协作。

  1. 当我们创建一个 Vue 页面的时候,会有一个 templete 以及响应对象。
  2. 首先 compiler 会编辑 templete 生成 render 函数,然后 reactivity 会初始化响应对象,接着 runtime 中的 renderer 会调用 render 函数返回 VNode,要注意 render 函数中可能会引用响应对象中的数据。renderer 拿到 VNode 调用 mount 函数使用 VNode 创建 Web页面。
  3. 接下来,当我们的响应对象发生变化的时候,如果 render 函数中引用了对应的数据,renderder 则会再次调用 render 函数获取新的 VNode,然后对比新老 VNode, 给到 path 函数,然后根据需要更新 Web页面。

初始化流程

在上文的 todomvc 例子中(文件在 packages/vue/examples/composition/todumvc.html),初始化的逻辑主要分为两步,调用 createApp 创建实例,然后调用实例上的 mount 方法进行挂载。
那接下来,我们提出两个问题,然后带着问题去读源码。

  1. 如何创建实例?创建的实例是什么样的?
  2. 挂载的过程是什么样的?

实例创建

我们首先还是使用命令 pnpm serve 启动本地服务,然后显示如下界面说明服务启动成功。 终端-服务启动成功.png

打开对应的本地服务地址

本地服务初始界面.png

接下来打开 packages/vue/examples/composition/todomvc 目录,进入 todomvc 示例页面。

todomvc-page.png

然后打开控制台,源代码面板,打开 todomvc 文件,在第 93 行打上断点(这里调试的过程后续不做赘述)。

获取 app 实例

然后我们可以找到 createApp 方法是在 packages/runtime-dom/src/index.ts 66行 中定义的。
该方法会调用 ensureRenderer 方法获取一个 renderer 实例,然后调用该实例的 createApp 方法,并将参数传入,最后获取一个 app 实例。
const app = ensureRenderer().createApp(...args) (67行)
然后会从 app 实例中解构出 mount 方法,并对 mount 方法进行扩展。(74行) 最后返回 app 实例(111行)。 到了这一步,我们已经浅显的知道了 createApp 的过程,但是真正的内容还没有看到,比如 ensureRenderer 方法做了什么,返回的 renderer 实例是什么样的,它的 createApp 又做了什么呢?

ensureRenderer

接下来我们去查找 ensureRenderer 的定义,发现就在当前文件的 42 行进行定义,而返回的 renderer 则是通过 createRenderer 方法创建,那接下来我们再去看一下 createRenderer 这个方法。

createRenderer

createRenderer 是在 @vue/runtime-core 中引入的。
再接下来,我们在 packages/runtime-core/src/renderer.ts 文件中找到了该方法的定义(290行)。

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

该方法又返回了调用 baseCreateRenderer 方法的结果。

baseCreateRenderer

createRenderer 方法下面就是 baseCreateRenderer 方法的定义,从 309 行一直到 2341 行。
最后返回一个对象

return {
  render,
  hydrate,
  createApp: createAppAPI(render, hydrate)
}

render 函数就是将传入 vnode 转为 真实 dom 并挂载到宿主对象上,如果 vnode 对应 dom 已经存在,则会走 path 新老 vdom 逻辑。
hydrate 这里我们略过,因为我还没搞懂 😅,这里的结果为 undefined
createApp 则是调用 createAppAPI 之后的返回值。
那么接下来就要看一下 createAppAPI 做了什么了。

createAppAPI

createAppAPI 方法在 runtime-core/src/apiCreateApp.ts 177行 定义。
该方法返回一个函数 createAppcreateApp 函数会在内部创建一个包含 use、mixin、component、directive、mount、unmount、provide 方法的对象并返回。

至此,我们了解到 createApp 方法是在 runtime-dom/src/index.ts 66 行 定义,返回的是一个 app 实例。而这个 app 实例 是调用 ensureRenderer().createApp(...args) 得到的,其中 ensureRenderer 会通过一系列调用返回一个包含 createApp 方法的对象。

Vue3初始化过程.png

挂载

挂载相对就简单很多了,其实就是调用 createApp 返回的 app 实例mount 方法,该方法会判断当前 app 实例 是否已挂载,如果没有挂载,则会创建对应的 vnode,然后通过 render 方法将 vnode 挂载到其宿主元素上,否则什么也不做。

本文主要介绍了 Vue3 源码的主体结构及初始化的流程,水平及时间有限,本文没有做特别详细的讲解,如有任何问题或建议,欢迎留言讨论!👏🏻👏🏻👏🏻

喜欢的话点个赞吧!

谢谢.gif