「这是我参与2022首次更文挑战的第36天,活动详情查看:2022首次更文挑战」
主体架构
代码组织
- TypeScript 代码编写
- rollup 打包
- jest 单元测试
- eslint 代码检查
- prettier 代码格式化
目录结构
- packages 代码包,里面主要分为以下三个大模块:
- compiler 编译相关
- reactivity 响应式
- runtime 运行时
- scripts 脚本
上文Vue 源码学习开篇!中我们使用 pnpm dev
进行了开发环境打包,并且添加了 --sourcemap
配置,这个命令会执行 scripts/dev.js
文件进行打包,接下来我们看一下打包这个动作 vue
到底都做了什么,然后基于它去理解其他的打包命令就可以了。
上图我对 scripts/dev.js
文件进行了逐行注释,大体逻辑是基于命令获取各种打包配置,然后利用 execa 执行打包脚本,并传入打包配置。
读懂了 pnpm dev
的逻辑,其他的打包命令基本就大同小异了。
模块依赖关系
目录结构部分提到 packages
代码包中主要分为以下三个大模块:
- compiler 编译相关
- reactivity 响应式
- runtime 运行时
所以这里我们讲主体的依赖关系就是下图所示:
上面是一个简单的主体依赖关系图,这里我们大致说一下一个 Vue
页面的运行,解释一下这三个模块是如何协作。
- 当我们创建一个
Vue
页面的时候,会有一个templete
以及响应对象。 - 首先
compiler
会编辑templete
生成render
函数,然后reactivity
会初始化响应对象,接着runtime
中的renderer
会调用render
函数返回VNode
,要注意render
函数中可能会引用响应对象中的数据。renderer
拿到VNode
调用mount
函数使用VNode
创建 Web页面。 - 接下来,当我们的响应对象发生变化的时候,如果
render
函数中引用了对应的数据,renderder
则会再次调用render
函数获取新的VNode
,然后对比新老VNode
, 给到path
函数,然后根据需要更新 Web页面。
初始化流程
在上文的 todomvc 例子中(文件在packages/vue/examples/composition/todumvc.html),初始化的逻辑主要分为两步,调用 createApp
创建实例,然后调用实例上的 mount
方法进行挂载。
那接下来,我们提出两个问题,然后带着问题去读源码。
- 如何创建实例?创建的实例是什么样的?
- 挂载的过程是什么样的?
实例创建
我们首先还是使用命令 pnpm serve
启动本地服务,然后显示如下界面说明服务启动成功。
打开对应的本地服务地址
接下来打开 packages/vue/examples/composition/todomvc
目录,进入 todomvc
示例页面。
然后打开控制台,源代码面板,打开 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行
定义。
该方法返回一个函数 createApp
,createApp
函数会在内部创建一个包含 use、mixin、component、directive、mount、unmount、provide
方法的对象并返回。
至此,我们了解到 createApp
方法是在 runtime-dom/src/index.ts 66 行
定义,返回的是一个 app 实例
。而这个 app 实例
是调用
ensureRenderer().createApp(...args)
得到的,其中 ensureRenderer
会通过一系列调用返回一个包含 createApp
方法的对象。
挂载
挂载相对就简单很多了,其实就是调用 createApp
返回的 app 实例
的 mount
方法,该方法会判断当前 app 实例
是否已挂载,如果没有挂载,则会创建对应的 vnode
,然后通过 render
方法将 vnode
挂载到其宿主元素上,否则什么也不做。
本文主要介绍了 Vue3
源码的主体结构及初始化的流程,水平及时间有限,本文没有做特别详细的讲解,如有任何问题或建议,欢迎留言讨论!👏🏻👏🏻👏🏻
喜欢的话点个赞吧!