携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第20天,点击查看活动详情 >>
版本:3.2.31
在首次渲染过程中,完成了根组件实例的挂载后,Vue3会将template的内容编译后存放在根组件实例的 render属性上(具体实现可参阅vue3源码解读之初始化流程中的finishComponentSetup)。然后在开始渲染根组件时执行当前根组件实例的render函数获取子元素的VNode,将子元素的VNode传入patch函数中,递归渲染子元素(具体实现可参阅vue3源码解读之初始化流程中的setupRenderEffect)。
<!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
) {
// If a vnode has non-null el, it means it's being reused.
// Only static vnodes can be reused, so its mounted DOM nodes should be
// exactly the same, and we can simply do a clone here.
// only do this in production since cloned trees cannot be HMR updated.
// 复用节点
el = vnode.el = hostCloneNode(vnode.el)
} else {
// 创建节点
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<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
)
}
}
/**
* Special case for setting value on DOM elements:
* - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024)
* - it needs to be forced (#1471)
* #2353 proposes adding another renderer option to configure this, but
* the properties affects are so finite it is worth special casing it
* here to reduce the complexity. (Special casing it also should not
* affect non-DOM renderers)
*/
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 中:
-
首先执行hostCreateElement创建该VNode的原生element元素。
-
接着创建当前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 {
// special case for <input v-model type="checkbox"> with
// :true-value & :false-value
// store value as dom properties since non-string values will be
// stringified.
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元素的原生方法,初始化其属性。
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中。