1. 调试环境搭建
- 下载代码
- 使用安装依赖
yarn
修改
package.json
,dev命令中添加--sourcemap
"dev": "node scripts/dev.js --sourcemap"
执行编译
yarn dev
,生成结果的路径:packages\vue\dist\vue.global.js
- 可使用
packages\vue\examples
,也可以自己编写 html,引入上面生成vue.global.js
文件,结合chrome 浏览器进行调试。 ```html
{{message}}

<a name="sinzf"></a>
# 2. 总览
<a name="i9B01"></a>
## 2.1 目录
源码在package文件夹内,采用了[monorepo](https://en.wikipedia.org/wiki/Monorepo) 管理项目<br />
- compiler-core 核心编译逻辑
- compiler-dom 针对浏览器平台编译逻辑
- compiler-sfc 针对单文件组件编译逻辑
- compiler-ssr 针对服务端渲染编译逻辑
- runtime-core 运行时核心
- runtime-dom 运行时针对浏览器的逻辑
- runtime-test 浏览器外完成测试环境仿真
- reactivity 响应式逻辑
- template-explorer 模板浏览器
- server-renderer 服务器端渲染
- share 公用方法
- vue 代码入口,整合编译器和运行时
包的依赖关系:<br />
<a name="WLdVk"></a>
## 2.2 Vue基本原理
Vue主要包含三大件:
- 响应式模块:主要是创建响应式的数据对象,观察这些数据的改变。
- 编译模块:将 template 模板编译到 render 函数
- 渲染模块:分为三个部分
- render 阶段:调用 render 函数返回虚拟DOM
- mount 阶段:将虚拟DOM挂载到页面元素上
- patch 阶段:将新旧的虚拟阶段进行比较,更新网页变化的部分
简单描述下大体流程就是:
1. 将 template 编译成 render 函数,然后使用响应式模块将数据进行初始化
2. 调用 render函数返回虚拟 dom,调用 mount 方法进行,创建web页面
3. 如果响应式数据发生变化,会再次调用渲染函数,创建新的虚拟 dom,执行 patch 操作,更新 web 页面
本文主要介绍渲染部分的内容
<a name="NUmFX"></a>
# 3. 源码分析
<a name="Xqpgz"></a>
## 3.1 ceateApp方法
```javascript
import { createApp } from 'vue'
const app = createApp({})
调用ceateApp()
返回一个应用实例,这其实是一个入口函数:
// 位置:runtime-dom/index.ts
const createApp = ((...args) => {
// 创建 app 对象
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重写 mount 方法
app.mount = (containerOrSelector) => {
// ...
}
return app
})
可见createApp
内部就创建了一个 app 对象,且提供了mount
方法。
进一步深入到内部:
ensureRenderer()
用来创建一个一个渲染器对象:
// 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
// 延时创建渲染器,当用户只依赖响应式包的时候,可以通过 tree-shaking 移除核心渲染逻辑相关的代码
let renderer
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
function createRenderer(options) {
return baseCreateRenderer(options)
}
function baseCreateRenderer(options) {
function render(vnode, container) {
// 组件渲染的核心逻辑
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
hydrate
为 SSR 相关参数先忽略,继续查看createAppAPI
方法:
function createAppAPI(render) {
// createApp createApp 方法接受的两个参数:根组件的对象和 props
return function createApp(rootComponent, rootProps = null) {
const app = {
...
// 创建默认APP配置
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
...
use() {...},
mixin() {...},
component() {...},
directive() {...},
// 重点看mount函数
mount(rootContainer) {
if (!isMounted) {
// 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// store app context on the root VNode.
vnode.appContext = context
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
isMounted = true
app._container = rootContainer
return vnode.component!.proxy
}
}
unmount() {...},
provide() {...}
}
//返回app实例
return app
}
3.2 mount 方法
我们在使用createApp({ ... }).mount('#app')
的mount
方法,不是直接执行上面的app实例里的mount
方法,其是属于runtime-core
包,是一个标准的可跨平台的组件流程。
mount
方法在rumtime-dom
包里的createApp()
方法里进行了重写,这是针对web平台,其他平台也可以对app.mount
进行重写以实现不同平台的渲染逻辑。
app.mount = (containerOrSelector) => {
// 标准化容器
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
// 如组件对象没有定义 render 函数和 template 模板,那就取DOM里面原本内容当作模版
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 挂载前清空容器内容
container.innerHTML = ''
// 真正的挂载,执行app.mount方法
return mount(container)
}
normalizeContainer
方法:可以传字符串选择器或者 DOM 对象字符串选择器,最终也会 DOM 对象,作为最终挂载的容器。
3.3 创建和渲染VNode
3.3.1 创建
VNode的概念应该都不陌生,就是Virtual DOM,其实就是JavaScript对象描述DOM:
<div>
<MyComponent />
</div>
const elementVNode = {
tag: 'div',
data: null,
children: {
tag: MyComponent,
data: null
}
}
回到app.mount方法,其中主要做了两件事,创建 VNode和渲染VNode
先来看创建VNode:
const vnode = createVNode(rootComponent, rootProps)
createVNode的函数如下:
function createVNode(type, props = null
,children = null) {
// 处理 props 相关逻辑,标准化 class 和 style
if (props) {
...
}
// 对 vnode 类型信息编码
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0
const vnode = {
type,
props,
shapeFlag,
// 一些其他属性
}
// 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
normalizeChildren(vnode, children)
return vnode
}
上述代码的流程为:
- 对 props 做标准化处理
- 对 VNode 的类型信息编码
- 创建 VNode 对象,标准化子节点 children 。
- 返回VNode
3.3.2 渲染
再回到app.mount方法 ,创建完VNode,接下来是渲染VNode:
// 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
查看render
方法:
// 位置:runtime-core/renderer.ts
const render = (vnode, container) => {
if (vnode == null) {
// 销毁组件
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 创建或者更新组件
patch(container._vnode || null, vnode, container)
}
// 缓存 vnode 节点,表示已经渲染
container._vnode = vnode
}
主要是查看pach
方法:
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
...
}
const { type, shapeFlag } = n2
// 先通过type来判断选择处理方法
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment: //Vue3新增
// 处理 Fragment 元素
break
default:
// 通过 shapeFlag编码判断
if (shapeFlag & 1 /* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 6 /* COMPONENT */) {
// 处理组件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 64 /* TELEPORT */) {
// 处理 TELEPORT,Vue3新增
}
else if (shapeFlag & 128 /* SUSPENSE */) {
// 处理 SUSPENSE,Vue3新增
}
}
}
patch
接受的前 3 个参数:
- n1 表示老的 VNode,为 null 时即为第一次挂载
- n2 表示新的 VNode节点
- container 为被挂载的DOM容器
处理组件
我们先查看处理组件的processComponent
:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
// 挂载组件
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
// 更新组件
updateComponent(n1, n2, parentComponent, optimized)
}
}
如果 n1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。
先看mountComponent
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 创建组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 设置组件实例
setupComponent(instance)
// 设置并运行带副作用的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
这个函数主要是创建和设置组件实例,设置并运行带副作用的渲染函数setupRenderEffect
:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// beforeMount hook
if (bm) { invokeArrayFns(bm) }
// 渲染组件生成subTree的vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 把子树 subTree挂载到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
// 保留渲染生成的子树根 DOM 节点
initialVNode.el = subTree.el
// mounted hook
if (m) {queuePostRenderEffect(m, parentSuspense)}
instance.isMounted = true
}
else {
// 更新组件
...
patch(prevTree, nextTree,
hostParentNode(prevTree.el),
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG)
...
}
}, prodEffectOptions)
}
effect
函数为数据响应式相关的函数,这个可以查看Vue3的reactive
办理,所谓副作用函数可以理解为当组件使用的数据发生变化时,effect
传入的渲染函数会被重新执行,来触发组件的更新。
更新的核心逻辑是传入新旧VNode执行patch
函数,其就是找出新旧VNode的不同,并找到合适的方式更新DOM。其中用到了大名鼎鼎的DOM diff算法,这里就先不讲了。
处理DOM
处理普通 DOM元素的processElement
和processComponent
的逻辑相同,n1为null时挂载,不为时更新。
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
//挂载元素节点
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
//更新元素节点
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
查看mountElement
:
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 创建 DOM 元素节点,其实就是 document.createElement
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
if (props) {
// 处理 props,比如 class、style、event 等属性
...
}
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
// 处理子节点是纯文本的情况,其实就是 el.textContent = text
hostSetElementText(el, vnode.children)
}
else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 处理子节点是数组的情况
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
}
// 把创建的 DOM 元素节点挂载到 container 上
hostInsert(el, container, anchor)
}
该函数主要做了:
- 创建 DOM 元素节点
- 处理 props
- 处理 children
- 挂载 DOM 元素到 container 上。
其中,如果子节点是数组,则执行 mountChildren
方法:
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
for (let i = start; i < children.length; i++) {
// 预处理 child
const child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]))
// 递归 patch 挂载 child
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
patch
是递归调用的, 通过这种深度优先遍历树的方式,我们就可以构造完整的 DOM 树,完成组件的渲染和更新。
新特性介绍
在patch方法中,先通过type和shareFlag来判断选择处理VNode,其中有几个新常量,这里简单做个说明:
Fragment
<template>
<div>Hello</div>
<div>World</div>
</template>
Vue2是没法这样创建组件,必须被一个标签包裹。而Vue3是可以的,生成的VNode是只有flag,没有tag的
const elementVNode = {
flags: VNodeFlags.FRAGMENT,
tag: null,
....
}
Teleport
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML
<!-- to 属性就是目标位置 -->
<teleport to="#teleport-target">
<div>test</div>
</teleport>
这个标签的作用是当你在进行一个异步加载时,可以先提供一些静态组件作为显示内容,然后当异步加载完毕时再显示。
<Suspense>
<template #default>
<UserProfile />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
3.4 Vue3的编译优化
Vue 是首先通过编译是把templete编译成render函数,然后再通过渲染生成DOM。
Vue3做到了在编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的。每个区块只需要追踪自身包含的动态节点。
还对diff算法进行重写,充分利用编译时的信息,使得性能得到了极大的提升。
官方的在线调试模板工具:https://vue-next-template-explorer.netlify.app/
Vue3还做了很多方面的优化,这里就不多介绍了(太菜,还看不懂)。
4.总结
本文主要介绍了Vue3 组件初始化以及渲染的过程,主要探究Vue源码的一个大体流程,对于响应式模块、编译器没有进行介绍,以后有时间进行整理,关于Vue3的新特性和优化以及对应的原理还需要时间进行学习和消化。
BTW:为更好的理解Vue的一个大体原理,根据一些课程和文章,写了一个mini-vue3的demo
https://github.com/mewcoder/codebase/blob/main/mini-vue3/index.html
最后来张图,拥抱Vue3吧!