vue3 源码学习:组件挂载为DOM过程
基础使用
vue3 使用 createApp
这个方法来创建并挂载组件,我们在 main 文件中引入 APP 根组件,通过 createApp
函数的 mount
方法将根组件挂载到 id 为 #app
这个节点下面
1// main 文件中引入根组件,通过 createApp 方法挂载根组件到 #app 节点
2import { createApp } from 'vue'
3import App from './App.vue'
4
5createApp(App).mount('#app')
所以通过 main 文件我们可以初步得到组件渲染为 DOM 主要分为两步
- 通过
createApp
方法创建组件实例 - 通过
mount
方法挂载元素
组件挂载为 DOM 实现原理
创建组件实例
createApp
方法定义在 packages/runtime-dom/src/index.ts 文件下,可以看到函数主要分为两个步骤:生成 app 实例和重写 mount 方法,我们首先先看 app 实例如何通过 ensureRenderer
方法生成
1export const createApp = ((...args) => {
2 // 调用 ensureRenderer 方法生成 app 实例
3 const app = ensureRenderer().createApp(...args)
4
5 // 从 app 实例中获取 mount 方法并重写
6 const { mount } = app
7 app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
8 // 重写 mount 方法过程
9 }
10
11 // 返回 app 实例
12 return app
13}) as CreateAppFunction<Element>
ensureRenderer
通过中间函数 createRenderer
的封装,最后会调用 baseCreateRenderer
方法,在 baseCreateRenderer
中定义了渲染过程的核心方法,并在最后返回了 render
和 createApp
这两个方法,而 createApp
使用的是 createAppAPI
1function ensureRenderer() {
2 return (
3 renderer ||
4 // 调用 createRenderer 方法,renderer 变量缓存结果
5 (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
6 )
7}
8
9export function createRenderer<
10 HostNode = RendererNode,
11 HostElement = RendererElement,
12>(options: RendererOptions<HostNode, HostElement>) {
13 // 调用 baseCreateRenderer 方法
14 return baseCreateRenderer<HostNode, HostElement>(options)
15}
16
17function baseCreateRenderer(
18 options: RendererOptions,
19 createHydrationFns?: typeof createHydrationFunctions
20): any {
21 // options 中包含了处理 DOM 元素常用的方法
22 const {
23 /*...*/
24 } = options
25
26 // 定义渲染过程核心方法,这里只列了暂时使用的 render 方法
27 const render = () => {
28 /*...*/
29 }
30
31 // 返回 render 和 createApp 方法
32 return {
33 render,
34 hydrate, // 服务端渲染使用
35 createApp: createAppAPI(render, hydrate),
36 }
37}
接下来我们来看 createAppAPI
方法的实现过程,整体过程主要分为三步
- 通过
createAppContext
创建 app 上下文 - 创建 app 实例,app 实例主要包含基础私有属性和实例方法,除了挂载过程使用的
mount
方法外,还有component
、directive
、mixin
等方法,这里不多做讨论 - 将 app 实例放在
createApp
方法中作为一个函数返回
1export function createAppAPI<HostElement>(
2 render: RootRenderFunction<HostElement>,
3 hydrate?: RootHydrateFunction
4): CreateAppFunction<HostElement> {
5 // 结果返回 createApp 函数
6 return function createApp(rootComponent, rootProps = null) {
7
8 // 创建 app 上下文
9 const context = createAppContext()
10
11 // 创建 app 实例
12 const app: App = (context.app = {
13 _uid: uid++,
14 _component: rootComponent as ConcreteComponent,
15 _props: rootProps,
16 _container: null,
17 _context: context,
18 _instance: null,
19
20 // 挂载方法
21 mount(rootContainer) { /*...*/ }
22 }
23
24 // 返回 app 实例
25 return app
26}
组件挂载过程
在 createApp
方法中创建完 app 实例之后,进入第二步重写 mount 方法,主要过程分为三步
- 通过
normalizeContainer
获取挂载的容器 - 获取模板的内容
- 清空 innerHTML ,创建挂载的代理方法并作为结果返回,代理方法通过 app 实例中的
mount
方法实现
1export const createApp = ((...args) => {
2 // 调用 ensureRenderer 方法生成 app 实例
3 const app = ensureRenderer().createApp(...args)
4
5 // 从 app 实例中获取 mount 方法并重写
6 const { mount } = app
7 app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
8 // 1.获取挂载容器
9 const container = normalizeContainer(containerOrSelector)
10 if (!container) return
11
12 // 2.获取模板内容
13 const component = app._component
14 if (!isFunction(component) && !component.render && !component.template) {
15 component.template = container.innerHTML
16 }
17
18 // 3. 挂载前清空 innerHTML 并创建挂载方法
19 container.innerHTML = ''
20 const proxy = mount(container, false, container instanceof SVGElement)
21
22 return proxy
23 }
24
25 // 返回 app 实例
26 return app
27}) as CreateAppFunction<Element>
虽然重写了 mount
方法,但是最后返回的代理方法仍然是在 app 实例中创建,app 实例中的 mount
方法方法同样也分为三步
- 通过
createVNode
方法创建 vnode 节点 - 通过
render
方法将 vnode 渲染为 DOM 节点 - 通过
getExposeProxy
暴露 expose 对象中的属性和方法
1// 创建 app 实例
2 const app: App = (context.app = {
3
4 // 挂载方法
5 mount(rootContainer) {
6 if (!isMounted) {
7 // 1. 创建一个 vnode 节点
8 const vnode = createVNode(
9 rootComponent as ConcreteComponent,
10 rootProps
11 )
12 vnode.appContext = context
13
14 // 2. 将 vnode 节点渲染为 DOM 节点
15 render(vnode, rootContainer, isSVG)
16
17 isMounted = true
18 app._container = rootContainer
19
20 // 3. TODO
21 return getExposeProxy(vnode.component!) || vnode.component!.proxy
22 }
23 }
24 }
创建 vnode
我们首先看第一步 createVNode
的实现过程,createVNode
函数调用的实际是 _createVNode
方法,_createVNode
方法在对节点属性做一些处理后,最后调用 createBaseVNode
方法生成 vnode
_createVNode
方法主要过程
- 如果当前节点是 vnode 类型,直接返回一个克隆的 vnode
- 标准化 props、class、style 相关属性
- 处理设置组件类型,主要包括:元素、Suspense、Teleport、组件、函数式组件
- 调用 createBaseVNode 创建 vnode
1function _createVNode(
2 type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
3 props: (Data & VNodeProps) | null = null,
4 children: unknown = null,
5 patchFlag: number = 0,
6 dynamicProps: string[] | null = null,
7 isBlockNode = false
8): VNode {
9 // 如果当前节点是 vnode 类型,直接返回一个克隆的 vnode
10 if (isVNode(type)) {
11 const cloned = cloneVNode(type, props, true /* mergeRef: true */)
12 // ...
13 return cloned
14 }
15
16 // 标准化 props、class、style 相关属性
17 if (props) {
18 // ...
19 }
20
21 // 处理设置组件类型,主要包括:元素、Suspense、Teleport、组件、函数式组件
22 const shapeFlag = isString(type)
23 ? ShapeFlags.ELEMENT
24 : __FEATURE_SUSPENSE__ && isSuspense(type)
25 ? ShapeFlags.SUSPENSE
26 : isTeleport(type)
27 ? ShapeFlags.TELEPORT
28 : isObject(type)
29 ? ShapeFlags.STATEFUL_COMPONENT
30 : isFunction(type)
31 ? ShapeFlags.FUNCTIONAL_COMPONENT
32 : 0
33
34 // 调用 createBaseVNode 创建 vnode
35 return createBaseVNode(
36 type,
37 props,
38 children,
39 patchFlag,
40 dynamicProps,
41 shapeFlag,
42 isBlockNode,
43 true
44 )
45}
在 createBaseVNode
方法中,定义 vnode 的主要属性,核心属性定义我放在了备注中
1function createBaseVNode() {
2 /* 相关参数 */
3 const vnode = {
4 __v_isVNode: true, // 内部属性,定义该对象是否是一个 vnode 对象
5 __v_skip: true, // 内部属性,是否跳过该节点的处理过程
6 type, // 节点类型
7 props, // 节点属性
8 key: props && normalizeKey(props), // 唯一标识符,优化 diff 算法
9 ref: props && normalizeRef(props), // 节点模板引用
10 scopeId: currentScopeId, // 节点的 scopeId,用于样式作用域
11 slotScopeIds: null, // 插槽作用域 Id 列表,处理插槽的样式作用域
12 children, // 子节点
13 component: null, // 节点是组件时,组件的实例对象
14 suspense: null, // 节点是 suspense 时,保存 suspense 的实例对象
15 ssContent: null, // 节点是 suspense 时,保存 suspense 的内容节点
16 ssFallback: null, // 节点是 suspense 时,保存 suspense 的 fallback 节点
17 dirs: null, // 当前节点所有指令
18 transition: null, // 过度对象
19 el: null, // 节点对应真实 DOM 元素
20 anchor: null, // 当前节点在父节点中的锚点元素,用于处理 patch 过程中的移动操作
21 target: null, // 当前节点的挂载目标,用于处理 teleport 组件
22 targetAnchor: null, // 当前节点在挂载目标中的锚点元素,用于处理 teleport 组件
23 staticCount: 0, // 当前节点及其子节点中静态节点的数量
24 shapeFlag, // 节点的类型标识符
25 patchFlag, // 优化 patch 过程标识符
26 dynamicProps, // 动态绑定属性
27 dynamicChildren: null, // 动态自己诶单
28 appContext: null, // 节点上下文,提供全局配置或者插件等操作
29 ctx: currentRenderingInstance,
30 } as VNode
31
32 return vnode
33}
这里要说明一下 vnode 属性中 type 和 shapFlag 的区别
-
type 定义的类型是唯一的,用于处理不同的 vnode,比如 type 为 text 表示节点是文本类型,为 component 表示节点是组件类型,不同类型需要调用不同的方法
-
shapeFlag 定义可能有多个,比如一个 VNode 表示一个带有
v-if
和v-for
的组件,那么它的 shapeFlag 一共有四个。shapeFlag 的作用在于优化渲染过程。在渲染过程中,根据shapeFlag
的不同,采用不同的渲染方式和算法,从而提高渲染性能1shapeFlag: ShapeFlags.STATEFUL_COMPONENT | 2 ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE | 3 ShapeFlags.SLOTS_CHILDREN | 4 ShapeFlags.STABLE_FRAGMENT 5// 表示当前节点是一个,有状态组件、子节点是插槽、需要跳过 keep-alive 缓存、子节点是一个稳定的片段
渲染为真实 DOM 过程
在创建完 vnode 节点后,下面进入第二步 render
,将 vnode 转换为真实 DOM 的过程。在 baseCreateRenderer
定义的 render
函数调用的是 patch
函数进行 DOM 挂载
1const render: RootRenderFunction = (vnode, container, isSVG) => {
2 if (vnode == null) {
3 // 如果 container 中存在 vnode 节点,需要先移除再挂载
4 if (container._vnode) {
5 unmount(container._vnode, null, null, true)
6 }
7 } else {
8 patch(container._vnode || null, vnode, container, null, null, null, isSVG)
9 }
10 // 更新 container 缓存的 vnode 节点
11 container._vnode = vnode
12}
在 patch
函数中会根据不同类型节点类型处理不同类型 (type 不同) 的 vnode 节点,其中最核心的是 processElement
处理元素类型和processComponent
处理组件类型。patch 过程主要分为以下五步
- 新旧节点相同直接返回
- 旧节点存在,且新旧节点类型不同,卸载旧节点
- PatchFlags.BAIL 时,会直接跳过该节点以及其后代节点的更新过程,以此优化性能
- 根据不同类型节点类型处理
- 定义 ref 模板引用(实现原理可以参考我的另一篇文章)
1const patch: PatchFn = (/* 相关参数 */) => {
2 // 新旧节点相同直接返回
3 if (n1 === n2) {
4 return
5 }
6
7 // 旧节点存在,且新旧节点类型不同,卸载旧节点
8 if (n1 && !isSameVNodeType(n1, n2)) {
9 anchor = getNextHostNode(n1)
10 unmount(n1, parentComponent, parentSuspense, true)
11 n1 = null
12 }
13
14 // PatchFlags.BAIL 时,会直接跳过该节点以及其后代节点的更新过程,以此优化性能
15 // 当前节点的 props 和 children 都没有发生变化,可以认为该节点不需要重新渲染
16 // 当前节点的子树中包含了一个被 keep-alive 组件包裹的节点,该节点被缓存起来并不需要更新,也可以直接跳过该节点以及其后代节点的更新过程
17 if (n2.patchFlag === PatchFlags.BAIL) {
18 optimized = false
19 n2.dynamicChildren = null
20 }
21
22 // 根据不同类型节点类型处理,核心:`processElement` 处理元素类型,`processComponent` 处理组件类型
23 const { type, ref, shapeFlag } = n2
24 switch (type) {
25 // 其他类型处理过程省略...
26 default:
27 // 其他类型处理过程省略...
28 if (shapeFlag & ShapeFlags.ELEMENT) {
29 processElement(/* 相关参数 */)
30 } else if (shapeFlag & ShapeFlags.COMPONENT) {
31 processComponent(/* 相关参数 */)
32 }
33 }
34
35 // 定义 ref 模板引用
36 if (ref != null && parentComponent) {
37 setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
38 }
39}
元素类型 vnode
在挂载节点时,n1 - 旧节点为 null,直接进入 mountElement
方法挂载元素节点。在 mountElement
方法处理过程包括
- 根据 vnode 节点,通过
hostCreateElement
创建 DOM 节点 - 调用
mountChildren
方法首先处理子节点,mountChildren
本质也是遍历调用patch
方法处理 vnode - 分别处理 vnode 指令、scopeId、props 等属性
- 通过
hostInsert
方法将创建好的 el 元素挂载到容器中
1const mountElement = (/* 相关参数 */) => {
2 let el: RendererElement
3 let vnodeHook: VNodeHook | undefined | null
4 const { type, props, shapeFlag, transition, dirs } = vnode
5
6 // 根据 vnode 创建 DOM 节点
7 el = vnode.el = hostCreateElement(
8 vnode.type as string,
9 isSVG,
10 props && props.is,
11 props
12 )
13
14 // 处理子子节点过程,子节点如果是文本类型直接挂载文本,否则通过 patch 方法继续处理
15 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
16 hostSetElementText(el, vnode.children as string)
17 } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
18 mountChildren(/* 相关参数 */)
19 }
20
21 // 处理 vnode 指令、scopeId、props 等属性
22 // ...
23
24 // 创建好的 el 元素挂载到容器中
25 hostInsert(el, container, anchor)
26}
组件类型 vnode
和元素类型节点类似,这里直接进入 mountComponent
方法挂载组件节点。mountComponent
实现过程主要有三步
- 通过
compatMountInstance
创建了一个组件实例 instance,instance 本质也是包含组件运行过程属性的对象 - 通过
setupComponent
方法初始化 instance 上的 props、slots、attrs、emit - 调用
setupRenderEffect
方法设置并运行带副作用的渲染函数
1const mountComponent: MountComponentFn = (/* 相关参数 */) => {
2 // 创建一个函数组件的实例
3 const instance: ComponentInternalInstance =
4 compatMountInstance ||
5 (initialVNode.component = createComponentInstance(
6 initialVNode,
7 parentComponent,
8 parentSuspense
9 ))
10
11 //...
12 // 初始化 instance 上的 props、slots、attrs、emit
13 setupComponent(instance)
14 //...
15
16 // 设置并运行带副作用的渲染函数
17 setupRenderEffect(/* 相关参数 */)
18}
在 setupRenderEffect
方法中,定义了一个 componentUpdateFn
并通过 ReactiveEffect
转换一个副作用的渲染函数,最后执行副作用函数的 update
方法完成组件渲染为 DOM 的过程
1const setupRenderEffect: SetupRenderEffectFn = (/* 相关参数 */) => {
2 const componentUpdateFn = () => {
3 /* 具体函数实现 */
4 }
5
6 // 创建副作用渲染函数
7 const effect = (instance.effect = new ReactiveEffect(
8 componentUpdateFn,
9 () => queueJob(update),
10 instance.scope // track it in component's effect scope
11 ))
12
13 const update: SchedulerJob = (instance.update = () => effect.run())
14 update.id = instance.uid
15 // allowRecurse
16 // #1801, #2043 component render effects should allow recursive updates
17 toggleRecurse(instance, true)
18
19 update()
20}
接下来看组件类型挂载实现的核心函数 componentUpdateFn
的实现,因为是第一次挂载,所以 isMounted 属性为 false,直接进入第一分支(以下只列了最为核心的函数实现),主要过程包括
- 通过
renderComponentRoot
方法渲染子树 vnode,renderComponentRoot
方法本质是执行了 instance 的render
方法,就是 vue 的 template 被编译后的 render 函数 - 通过
patch
方法挂载子树到 container,并标记当前组件已挂载
1const componentUpdateFn = () => {
2 if (!instance.isMounted) {
3 // 渲染子树 vnode
4 const subTree = (instance.subTree = renderComponentRoot(instance))
5
6 // 挂载子树到 container
7 patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
8 initialVNode.el = subTree.el
9
10 // 标记当前组件已挂载
11 instance.isMounted = true
12 }
13}
组件挂载 DOM 过程回顾
最后再回顾一下组件挂载为 DOM 的整体流程
- 创建 app 实例,通过入口函数
createApp
,最终调用createAppAPI
方法创建 app 实例对象 - 调用
mount
方法,通过重写 app 实例对象定义的mount
方法- 通过
createVNode
创建虚拟节点 vnode - 通过
render
方法,调用patch
方法处理不同类型的 vnode,核心包括processElement
处理元素类型节点和processComponent
处理组件类型节点
- 通过
整体挂载过程如下图所示