版本:v3.3.4
在首次渲染过程中,完成了根组件实例的挂载后,Vue3
会将template
的内容编译后存放在根组件实例的 render
属性上(具体实现可参阅vue3 源码解读之初始化流程中的 finishComponentSetup)。然后在开始渲染根组件时执行当前根组件实例的render
函数获取子元素的VNode
,将子元素的VNode
传入patch
函数中,递归渲染子元素(具体实现可参阅vue3 源码解读之初始化流程中的 setupRenderEffect),将VNode
转换成真实DOM
,渲染到界面上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{title}}</h1>
</div>
<script src="../../dist/vue.global.js"></script>
<script>
// 1.创建实例
// vue3: createApp()
const { createApp } = Vue;
// 传入根组件配置
const app = createApp({
data() {
return {
title: "hello,vue3!",
};
},
}).mount("#app");
</script>
</body>
</html>
在上面的HTML
代码中,根组件实例的template
如下:
执行根组件实例的render
函数后获取的子元素VNode
则如下图:
将该VNode
传入patch
函数中,开始渲染子元素。
// 进入 Diff 过程,将子树渲染到container中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
patch
// core/packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// ...
const { type, ref, shapeFlag } = n2;
switch (type) {
// ...
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
);
}
// ...
}
// ...
};
在上面的subTree
中,type
为h1
,即元素类型为 ELEMENT
,因此会进入processElement
函数,执行 patch
过程。
流程图
总的流程图如下👇:
下面我们来分析下每个步骤做的事情,逐一击破💥:
processElement
// core/packages/runtime-core/src/renderer.ts
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
// 首次渲染执行 mountElement 挂载 ELEMENT
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 更新过程
}
}
由于是首次渲染,因此执行 mountElement
函数渲染 ELEMENT
类型的元素。
mountElement
// core/packages/runtime-core/src/renderer.ts
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
if (
!__DEV__ &&
vnode.el &&
hostCloneNode !== undefined &&
patchFlag === PatchFlags.HOISTED
) {
// 判断当前 VNode 是否是静态节点,并且是否已经被挂载到 DOM 上。如果是,则说明当前 VNode 可以被复用,只需要对已有的 DOM 元素进行克隆即可。
// 在复用节点时,需要注意只有静态节点才能被复用,因此需要判断当前 VNode 是否是静态节点。此外,只有在生产环境下才能进行节点克隆,因为克隆的节点无法进行热更新。
el = vnode.el = hostCloneNode(vnode.el)
} else {
// 创建节点
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// 元素节点可以包含文本子节点和元素子节点。在挂载元素节点时,需要先挂载子节点,因为某些属性可能依赖于子节点的内容,例如 <select> 元素的 value 属性
// 判断当前节点是否包含文本子节点。如果是,则直接设置节点的文本内容。
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 设置节点的文本内容
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 当前节点下还有子节点,则向下递归挂载子节点
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
// 如果元素有属性,则初始化这些属性
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
/**
* 首先判断当前节点的 props 对象中是否包含 value 属性。如果包含,则需要对 value 属性进行特殊处理。
在处理 value 属性时,需要注意以下两点:
1、value 属性的设置顺序可能会影响表单元素的最终值。例如,在设置 min 和 max 属性后再设置 value 属性,可能会导致 value 属性的值被覆盖。因此,在设置 value 属性时,需要保证它是在其他相关属性之后设置的。
2、value 属性的更新可能需要被强制执行。例如,在某些情况下,表单元素的值可能会被外部代码修改,此时需要强制更新 value 属性的值。因此,在设置 value 属性时,需要使用 hostPatchProp 函数,并将第三个参数设置为 null,以确保 value 属性的更新能够被强制执行。
*/
if ('value' in props) {
hostPatchProp(el, 'value', null, props.value)
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// scopeId
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
}
// ...
// 将当前节点追加到父元素里
hostInsert(el, container, anchor)
// ...
}
在 mountElement
中:
- 在复用节点时,需要注意只有静态节点才能被复用,因此需要判断当前 VNode 是否是静态节点。此外,只有在生产环境下才能进行节点克隆,因为克隆的节点无法进行热更新。
- 首先执行
hostCreateElement
创建该VNode
的原生element
元素。 - 然后需要注意:元素节点可以包含文本子节点和元素子节点。在挂载元素节点时,需要先挂载子节点,因为某些属性可能依赖于子节点的内容,例如
<select>
元素的value
属性 - 接着创建当前
VNode
的子节点,如果当前VNode
的子节点是文本节点,则调用hostSetElementText
设置当前节点的文本内容;如果当前节点下还有多个子节点,则调用mountChildren
,进入patch
流程,向下递归挂载子节点。 - 如果当前
VNode
上有props
,则调用hostPatchProp
初始化当前元素的props
属性。 - 当前元素的属性都已经初始化完并且其子节点都已经挂载完,则将当前元素追加到父元素
container
中
hostCreateElement
// core/packages/runtime-dom/src/nodeOps.ts
// hostCreateElement 其实执行的是 createElement
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
执行 hostCreateElement
创建 HTML
元素,其实执行的是 nodeOps
的 createElement
,在 createElement
中,调用了 document
的方法来创建 HTML
元素。
hostSetElementText
// core/packages/runtime-dom/src/nodeOps.ts
// hostSetElementText 其实执行的是 setElementText
setElementText: (el, text) => {
el.textContent = text
},
逻辑比较简单,执行 hostSetElementText
设置节点的文本内容,其实执行的是 nodeOps
的 setElementText
,通过节点的 textContent
属性来设置节点的文本内容。
mountChildren
// core/packages/runtime-core/src/renderer.ts
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
start = 0
) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
在 mountChildren
中,遍历子节点,进入 patch
流程,向下递归挂载子节点。
hostPatchProp
// core/packages/runtime-dom/src/patchProp.ts
// hostPatchProp 实际上执行的 patchProp
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
if (key === 'class') {
patchClass(el, nextValue, isSVG)
} else if (key === 'style') {
patchStyle(el, prevValue, nextValue)
} else if (isOn(key)) {
// ignore v-model listeners
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
} else if (
key[0] === '.'
? ((key = key.slice(1)), true)
: key[0] === '^'
? ((key = key.slice(1)), false)
: shouldSetAsProp(el, key, nextValue, isSVG)
) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
)
} else {
// 判断当前属性的键是否为 true-value 或 false-value。如果是,则说明当前属性是用于设置复选框元素的选中值的。
if (key === 'true-value') {
;(el as any)._trueValue = nextValue
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
patchAttr(el, key, nextValue, isSVG, parentComponent)
}
}
在初始化元素的 props
时,根据属性名,调用 DOM
元素的原生方法,初始化其属性。
但是在处理复选框元素的选中值时,需要注意以下几点:
- 复选框元素的选中值可能是非字符串类型,例如布尔值、数字等。为了确保选中值能够正确地被存储,需要将选中值存储在
DOM
属性中,而不是存储在VNode
的props
对象中。在上面代码中,使用了_trueValue
和_falseValue
属性来存储复选框元素的选中值。 - 复选框元素的选中值可能会被序列化为字符串。为了避免这种情况,需要将选中值存储在
DOM
属性中,并在更新时从DOM
属性中读取选中值。
hostInsert
// core/packages/runtime-dom/src/nodeOps.ts
// hostInsert 实际上执行的是 insert
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
执行 hostInsert
,实际执行的是 nodeOps
的 insert
,通过节点的 insertBefore
方法,将子节点的内容插入到父节点中。
总结
在首次渲染过程中,完成根组件实例的挂载后,获取 template
的虚拟 DOM
,将其传入 patch
函数中,递归渲染子元素。在子元素的渲染过程中,会首先创建节点,然后创建当前节点的子元素。如果当前节点上有 props
,则初始化当前节点的 props
属性。最后将当前元素插入到到父元素 container
中。