本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue3 源码解析系列第 11 篇,关注专栏
前言
上篇我们分析了 render
函数对 HTML
标签属性、 DOM
属性、Style
样式、Event
事件的挂载更新,那么对于 Vue 特殊的 DOM
类型,比如 Text
、Comment
、Fragment
类型是如何渲染更新的呢?下面我们就来逐一分析。
案例一
首先引入 h
、 render
函数和 Text
类型,先渲染类型为 Text
的 vnode1
元素,两秒后修改子节点内容,渲染相同类型的 vnode2
元素。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render, Text } = Vue
const vnode1 = h(Text, 'hello world')
render(vnode1, document.querySelector('#app'))
setTimeout(() => {
const vnode2 = h(Text, '你好世界')
render(vnode2, document.querySelector('#app'))
}, 2000)
</script>
</body>
</html>
render Text 类型
我们知道 render
函数的渲染主要执行了 patch
方法:
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) {
case Text:
processText(n1, n2, container, anchor)
break
// 省略
default:
// 省略
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
当前 type
为 Text
类型即 Symbol(Text)
:
接着执行 processText
方法:
ts
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
可以看出该方法先创建一个 Text
节点然后通过 hostInsert
方法插入到页面中,我们再看下 hostCreateText
方法,实际执行的是 createText
,它被定义在 packages/runtime-dom/src/nodeOps.ts
文件中:
ts
createText: text => doc.createTextNode(text)
之后页面呈现:
两秒后更新节点,再次执行 processText
方法,我们主要关注下面这段逻辑:
ts
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
// 省略
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
由于当前新旧子节点不同,执行 hostSetText
方法,实际执行的是 setText
,它被定义在 packages/runtime-dom/src/nodeOps.ts
文件中:
ts
setText: (node, text) => {
node.nodeValue = text
}
重新赋值新子节点后,页面呈现:
最后再将新节点赋值给旧节点 _vnode
,render
函数执行完成。
案例二
首先引入 h
、 render
函数和 Comment
类型,然后通过 render
函数渲染类型为 Comment
的 vnode
元素。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render, Comment } = Vue
const vnode = h(Comment, 'hello world')
render(vnode, document.querySelector('#app'))
</script>
</body>
</html>
render Comment 类型
由于注释节点不存在更新的问题,所以我们重新再看下 patch
方法:
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) {
// 省略
case Comment:
processCommentNode(n1, n2, container, anchor)
break
// 省略
default:
// 省略
}
// 省略
}
当前 Type
类型为 Comment
即 Symbol(Comment)
,执行 processCommentNode
方法:
ts
const processCommentNode: ProcessTextOrCommentFn = (
n1,
n2,
container,
anchor
) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateComment((n2.children as string) || '')),
container,
anchor
)
} else {
// there's no support for dynamic comments
n2.el = n1.el
}
}
该方法同 processText
相似,通过创建一个 Comment
节点然后插入到页面中,我们再来看下 hostCreateComment
方法,实际执行的是 createComment
,它被定义在 packages/runtime-dom/src/nodeOps.ts
文件中:
ts
createComment: text => doc.createComment(text)
之后页面呈现:
案例三
首先引入 h
、 render
函数和 Fragment
类型,先渲染类型为 Fragment
的 vnode1
元素,两秒后修改子节点内容,渲染相同类型的 vnode2
元素。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render, Fragment } = Vue
const vnode1 = h(Fragment, ['hello', ' world'])
render(vnode1, document.querySelector('#app'))
setTimeout(() => {
const vnode2 = h(Fragment, ['你好', ' 世界'])
render(vnode2, document.querySelector('#app'))
}, 2000)
</script>
</body>
</html>
render Fragment 类型
我们继续看下 patch
方法:
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) {
// 省略
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
// 省略
}
// 省略
}
当前 Type
类型为 Fragment
即 Symbol(Fragment)
,执行 processFragment
方法:
ts
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// 省略
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// 省略
} else {
// 省略
}
}
}
根据判断逻辑,由于初次渲染旧节点 n1
不存在,之后执行 mountChildren
方法:
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
)
}
}
需要注意的是这里 children
是数组类型即 案例三
传入的 ['hello', ' world']
。之后遍历数组,先取第一个元素执行 normalizeVNode(children[i]))
即 normalizeVNode('hello')
,我们再看下 normalizeVNode
方法:
ts
export function normalizeVNode(child: VNodeChild): VNode {
if (child == null || typeof child === 'boolean') {
// empty placeholder
return createVNode(Comment)
} else if (isArray(child)) {
// fragment
return createVNode(
Fragment,
null,
// #3666, avoid reference pollution when reusing vnode
child.slice()
)
} else if (typeof child === 'object') {
// already vnode, this should be the most common since compiled templates
// always produce all-vnode children arrays
return cloneIfMounted(child)
} else {
// strings and numbers
return createVNode(Text, null, String(child))
}
}
当前 child
存在且为字符串类型,最终执行 createVNode(Text, null, String(child))
,该方法我们在 Vue3源码解析之 h 文章中已经讲过,最终返回的是一个 Text
类型的 虚拟 DOM
,之后再通过 patch
方法渲染到页面中:
第二次执行相同的逻辑,最终渲染:
两秒后重新更新节点,再次执行 processFragment
方法:
ts
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// 省略
if (n1 == null) {
// 省略
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// 省略
} else {
// keyed / unkeyed, or manual fragments.
// for keyed & unkeyed, since they are compiler generated from v-for,
// each child is guaranteed to be a block so the fragment will never
// have dynamicChildren.
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
当前旧节点 n1
存在,执行 patchChildren
方法:
ts
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// fast path
if (patchFlag > 0) {
// 省略
}
// children has 3 possibilities: text, array or no children.
// 新节点为 text 节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 省略
} else {
// 旧节点为 array 节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// prev children was array
// 新节点为 array 节点
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// 省略
}
}
}
根据判断,当前新旧子节点都为数组类型,执行 patchKeyedChildren
方法:
ts
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])) // 转为 vnode
// 新旧节点类型是否相同
if (isSameVNodeType(n1, n2)) {
// 更新节点
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
i++
}
// 省略
}
由于节点类型相同,之后遍历执行 patch
方法,第一次更新完显示:
第二次更新完显示:
最后将新节点赋值给旧节点 _vnode
,render
函数执行完毕。
总结
Text
类型的渲染更新实际执行的是processText
方法,该方法主要通过createText
方法来创建文本节点,通过setText
方法来修改文本内容。Comment
类型的渲染实际执行的是processCommentNode
方法,该方法主要通过hostCreateComment
方法来创建注释节点。Fragment
类型的渲染更新实际执行的是processFragment
方法,该方法主要通过mountChildren
方法创建节点,通过patchChildren
方法更新节点。另外子节点的类型传入必须是一个数组,通过normalizeVNode
方法创建了一个Text
类型的虚拟 DOM
。