3.render
创建完vnode节点后,我们开始进入render也就是渲染阶段,也就是app实例里的render方法
- vnode : 要渲染的虚拟节点
- container : 挂载的DOM容器
- namespace : 命名空间(用于SVG/MathML)
const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) {
//当 vnode 为null时,表示要卸载当前容器中的内容,调用 unmount 函数
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
//当 vnode 存在时,调用 patch 函数进行更新或挂载
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace,
)
}
//通过 isFlushing 标志防止重复刷新
if (!isFlushing) {
isFlushing = true
//调用 flushPreFlushCbs() 和 flushPostFlushCbs() 处理预刷新和后刷新回调
flushPreFlushCbs()
flushPostFlushCbs()
isFlushing = false
}
container._vnode = vnode
}
1.patch
optimized 标志控制是否使用Block Tree优化
switch各节点的type来进行对应处理
相同节点快速返回避免不必要计算
const patch: PatchFn = (
n1, // 旧VNode (null表示首次挂载)
n2, // 新VNode
container, // 挂载容器
anchor = null, // 锚点位置
parentComponent = null, // 父组件实例
parentSuspense = null, // Suspense边界
namespace = undefined, // 命名空间(svg/mathml)
slotScopeIds = null, // 插槽作用域ID
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // 优化模式
) => {
// 1. 相同节点快速返回
if (n1 === n2) return
// 2. 节点类型不同时卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 3. 处理BAIL标记(强制全量diff)
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
// 4. 根据节点类型分发处理
const { type, ref, shapeFlag } = n2
switch (type) {
//文本/注释节点:直接DOM操作
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
// 静态节点:开发环境HMR更新
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, namespace)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, namespace)
}
break
//Fragment:处理多根节点
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
break
default:
// 5. 处理元素
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
//组件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
//Teleport:跨DOM树渲染
;(type as typeof TeleportImpl).process(n1 as TeleportVNode, n2 as TeleportVNode, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// Suspense:异步组件处理
;(type as typeof SuspenseImpl).process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// 6. 处理ref引用
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
1. processText 文本节点处理
非空断言 n1.el! 表明更新路径必有已挂载节点
hostCreateText / hostSetText 是跨平台抽象接口,在浏览器环境下对应 document.createTextNode 和 node.nodeValue 设置
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) { // 首次挂载
hostInsert( // 创建并插入新文本节点
(n2.el = hostCreateText(n2.children as string)), // 创建DOM文本节点并赋值给vnode.el
container,
anchor,
)
} else { // 更新
const el = (n2.el = n1.el!) // 复用已有DOM节点
if (n2.children !== n1.children) { // 文本内容变化时才更新
hostSetText(el, n2.children as string) // 高效更新文本内容
}
}
}
2.processCommentNode 注释节点
hostCreateComment是跨平台抽象接口- 在浏览器环境下对应
document.createComment
const processCommentNode: ProcessTextOrCommentFn = (
n1, // 旧VNode (null表示首次挂载)
n2, // 新VNode
container, // 挂载容器
anchor, // 锚点位置
) => {
if (n1 == null) { // 首次挂载路径
hostInsert( // 创建并插入新注释节点
(n2.el = hostCreateComment((n2.children as string) || '')), // 创建DOM注释节点并赋值给vnode.el
container,
anchor,
)
} else { // 更新路径
// 不支持动态更新注释内容
n2.el = n1.el // 直接复用已有DOM节点
}
}
3.mountStaticNode 静态节点
- 专门用于处理编译器生成的静态节点
- 通过 hostInsertStaticContent 平台方法批量插入
const mountStaticNode = (
n2: VNode, // 要挂载的静态VNode
container: RendererElement, // 容器元素
anchor: RendererNode | null, // 锚点位置
namespace: ElementNamespace, // 命名空间(svg/mathml)
) => {
// 静态节点仅在compiler-dom/runtime-dom中使用
// 这保证了hostInsertStaticContent的存在
;[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string, // 静态内容字符串
container,
anchor,
namespace,
n2.el, // 可选:已有开始节点
n2.anchor, // 可选:已有结束节点
)
}
hostInsertStaticContent函数
- 缓存路径:当已有节点可复用时,直接克隆插入(性能优化)
- 全新插入:使用template元素批量处理HTML字符串
insertStaticContent(content, parent, anchor, namespace, start, end) {
// 1. 确定插入位置参考节点
const before = anchor ? anchor.previousSibling : parent.lastChild
// 2. 缓存路径判断条件:
// - 单根节点
// - 兄弟节点信息可用
if (start && (start === end || start.nextSibling)) {
// 3. 缓存路径:克隆现有节点
while (true) {
parent.insertBefore(start!.cloneNode(true), anchor)
if (start === end || !(start = start!.nextSibling)) break
}
} else {
// 4. 全新插入路径
templateContainer.innerHTML =
namespace === 'svg' ? `<svg>${content}</svg>` :
namespace === 'mathml' ? `<math>${content}</math>` :
content
const template = templateContainer.content
// 5. 处理SVG/MathML命名空间
if (namespace === 'svg' || namespace === 'mathml') {
const wrapper = template.firstChild!
while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild)
}
template.removeChild(wrapper)
}
parent.insertBefore(template, anchor)
}
// 6. 返回插入的节点范围
return [
before ? before.nextSibling! : parent.firstChild!,
anchor ? anchor.previousSibling! : parent.lastChild!
]
}
4.processFragment 处理多根节点
const processFragment = (
n1, // 旧VNode (null表示首次挂载)
n2, // 新VNode
container, // 容器元素
anchor, // 锚点位置
parentComponent, // 父组件实例
parentSuspense, // Suspense边界
namespace, // 命名空间(svg/mathml)
slotScopeIds, // 插槽作用域ID
optimized, // 优化模式
) => {
// 1. 创建/复用Fragment锚点节点
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
// 2. 处理开发环境特殊情况
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (__DEV__ && (isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)) {
patchFlag = 0
optimized = false
dynamicChildren = null
}
// 3. 合并插槽作用域ID
if (fragmentSlotScopeIds) {
slotScopeIds = slotScopeIds
? slotScopeIds.concat(fragmentSlotScopeIds)
: fragmentSlotScopeIds
}
// 4. 分路径处理
if (n1 == null) {
// 4.1 首次挂载路径
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
//递归children执行patch
mountChildren(
(n2.children || []) as VNodeArrayChildren, // 处理空Fragment情况
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
// 4.2 更新路径
if (patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
n1.dynamicChildren) {
// 4.2.1 稳定Fragment优化路径
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
)
// 开发环境HMR处理
if (__DEV__) {
traverseStaticChildren(n1, n2)
} else if (n2.key != null ||
(parentComponent && n2 === parentComponent.subTree)) {
traverseStaticChildren(n1, n2, true /* shallow */)
}
} else {
// 4.2.2 常规更新路径
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
}
mountChildren其实就是递归children执行patch
patchBlockChildren
批量更新block节点
跳过常规的diff算法,直接按索引对比
const patchBlockChildren: PatchBlockChildrenFn = (
oldChildren, // 旧子节点数组
newChildren, // 新子节点数组
fallbackContainer, // 备用容器
parentComponent, // 父组件实例
parentSuspense, // Suspense边界
namespace: ElementNamespace, // 命名空间
slotScopeIds, // 插槽作用域ID
) => {
// 1. 遍历新子节点数组
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// 2. 确定patch容器
const container =
// 2.1 处理特殊情况:
// - 异步组件错误
// - Fragment节点
// - 不同类型节点
// - 组件或Teleport节点
oldVNode.el &&
(oldVNode.type === Fragment ||
!isSameVNodeType(oldVNode, newVNode) ||
oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
? hostParentNode(oldVNode.el)! // 获取实际父节点
: fallbackContainer // 使用备用容器
// 3. 执行patch
patch(
oldVNode,
newVNode,
container,
null, // anchor设为null
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
true, // 强制优化模式
)
}
}
5.子节点diff算法patchChildren 常规更新子元素
子节点diff算法核心算法
KEYED_FRAGMENT标记:使用 patchKeyedChildren 处理带key的子节点,采用双端diff算法UNKEYED_FRAGMENT标记:使用 patchUnkeyedChildren 处理无key子节点,按索引顺序比对
子节点类型处理矩阵 :
| 旧子节点类型 | 新子节点类型 | 处理方式 |
|---|---|---|
| 数组 | 文本 | 卸载旧数组 + 设置新文本 |
| 文本 | 数组 | 清空文本 + 挂载新数组 |
| 数组 | 数组 | 完整diff算法 |
| 文本/空 | 文本/空 | 直接更新文本 |
const patchChildren: PatchChildrenFn = (
n1, // 旧VNode
n2, // 新VNode
container, // 容器元素
anchor, // 锚点位置
parentComponent, // 父组件实例
parentSuspense, // Suspense边界
namespace: ElementNamespace, // 命名空间
slotScopeIds, // 插槽作用域ID
optimized = false, // 优化模式
) => {
// 1. 获取新旧子节点和shapeFlag
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// 2. 大于0即为动态节点
if (patchFlag > 0) {
// 2.1 处理带key的子节点
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
}
// 2.2 处理不带key的子节点
else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
}
}
// 新节点是文本节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果旧子节点是数组时,执行递归卸载
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
//如果新旧不同则重新创建
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 3.2 旧子节点是数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新旧都是数组,执行完整diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
// 新子节点不是数组,卸载旧子节点
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// 3.3 旧子节点是文本或null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// 新子节点是数组则挂载
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
}
}
1.patchKeyedChildren 处理带key的子节点
实现了双端diff算法
同步头部时,函数通过while循环从前往后比较节点,直到遇到不同的节点
同步尾部,类似地从后往前比较,直到节点不同
当旧节点遍历完而新节点还有剩余时,说明有新增节点,需要挂载到正确的位置
当新节点遍历完而旧节点还有剩余时,说明有节点需要卸载,调用unmount函数进行移除
其余的需要构建新旧节点的key映射,查找可复用的节点,并处理节点的移动和新增
const patchKeyedChildren = (
c1: VNode[], // 旧子节点数组
c2: VNodeArrayChildren, // 新子节点数组
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// 核心算法分5个阶段:
// 1. 头部同步(从前向后比对)
while (i <= e1 && i <= e2) {
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, ...)
} else break
i++
}
// 2. 尾部同步(从后向前比对)
while (i <= e1 && i <= e2) {
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, ...)
} else break
e1--; e2--;
}
// 3. 新增节点挂载
if (i > e1) {
while (i <= e2) {
patch(null, c2[i], container, anchor)
i++
}
}
// 4. 旧节点卸载
else if (i > e2) {
while (i <= e1) {
unmount(c1[i])
i++
}
}
// 5. 未知序列处理(核心diff逻辑)
else {
// 5.1 构建key到新索引的映射表
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
keyToNewIndexMap.set(c2[i].key, i)
}
// 5.2 新旧节点匹配
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
const newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
unmount(prevChild)
} else {
// 记录新旧索引映射
newIndexToOldIndexMap[newIndex - s2] = i + 1
}
}
// 5.3 最长递增子序列优化移动
const sequence = getSequence(newIndexToOldIndexMap)
let j = sequence.length - 1
for (i = toBePatched - 1; i >= 0; i--) {
if (newIndexToOldIndexMap[i] === 0) {
// 挂载新节点
patch(null, nextChild, ...)
} else if (moved) {
// 根据LIS决定移动策略
if (j < 0 || i !== sequence[j]) {
move(nextChild, ...)
} else j--
}
}
}
}
2.patchUnkeyedChildren 无key diff
仅按索引顺序比较新旧子节点
const patchUnkeyedChildren = (
c1: VNode[], // 旧子节点数组
c2: VNodeArrayChildren, // 新子节点数组
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
// 1. 初始化处理
c1 = c1 || EMPTY_ARR // 确保旧数组非空
c2 = c2 || EMPTY_ARR // 确保新数组非空
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength) // 计算公共长度
// 2. 公共部分patch
let i
for (i = 0; i < commonLength; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode) // 优化模式克隆节点
: normalizeVNode(c2[i])) // 普通模式标准化节点
patch(
c1[i],
nextChild,
container,
null, // anchor设为null
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
// 3. 处理剩余部分
if (oldLength > newLength) {
// 3.1 卸载旧节点
unmountChildren(
c1,
parentComponent,
parentSuspense,
true, // 执行remove操作
false, // 不跳过ref卸载
commonLength, // 起始索引
)
} else {
// 3.2 挂载新节点
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
commonLength, // 起始索引
)
}
}
1.卸载函数 unmountChildren
递归调用unmount进行卸载节点
const unmount: UnmountFn = (
vnode, // 要卸载的虚拟节点
parentComponent, // 父组件实例
parentSuspense, // Suspense边界
doRemove = false, // 是否执行DOM移除
optimized = false // 优化模式
) => {
// 核心逻辑分为六个阶段:
// 1. 引用清理
if (ref != null) {
setRef(ref, null, parentSuspense, vnode, true)
}
// 2. KeepAlive组件特殊处理
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
}
//通过位运算检查当前节点是否为DOM元素且包含指令
const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs
//排除异步组件包装器
const shouldInvokeVnodeHook = !isAsyncWrapper(vnode)
// 3. 生命周期钩子触发
// 从props中获取 onVnodeBeforeUnmount 钩子 ,并执行其函数
if (shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeBeforeUnmount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
// 4. 组件卸载核心逻辑
if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode.component!, parentSuspense, doRemove)
} else {
// 处理Suspense/Teleport等特殊节点
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
vnode.suspense!.unmount(parentSuspense, doRemove)
return
}
// 5. 动态子节点快速卸载
if (dynamicChildren && (type !== Fragment || ...)) {
unmountChildren(dynamicChildren, parentComponent, parentSuspense, false, true)
}
// 6. DOM元素最终移除
if (doRemove) {
remove(vnode)
}
}
// 7. 异步清理钩子
queuePostRenderEffect(() => {
// 触发unmounted生命周期
}, parentSuspense)
}
2.组件卸载 unmountComponent
该函数是组件卸载核心函数,确保组件卸载时的资源释放和状态管理
graph TD
A[beforeUnmount] --> B[Vue2 beforeDestroy]
B --> C[停止响应式]
C --> D[卸载子树]
D --> E[queue unmounted]
const unmountComponent = (
instance: ComponentInternalInstance, // 要卸载的组件实例
parentSuspense: SuspenseBoundary | null, // 父级Suspense边界
doRemove?: boolean // 是否执行DOM移除
) => {
// 1. HMR注销(开发模式专用)
if (__DEV__ && instance.type.__hmrId) {
unregisterHMR(instance) // 注销热更新
}
// 2. 提取关键实例属性
const { bum, scope, update, subTree, um } = instance
// 3. beforeUnmount生命周期
if (bum) { // beforeUnmount钩子数组
invokeArrayFns(bum) // 顺序执行所有beforeUnmount钩子
}
// 4. Vue 2兼容处理
if (__COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)) {
instance.emit('hook:beforeDestroy') // Vue 2风格的beforeDestroy事件
}
// 5. 停止响应式追踪
scope.stop() // 停止组件作用域内的所有响应式effect
// 6. 卸载子组件树
if (update) { // 渲染effect存在时
update.active = false // 禁用更新队列
unmount(subTree, instance, parentSuspense, doRemove) // 递归卸载虚拟DOM树
}
// 7. 异步执行unmounted钩子
if (um) { // unmounted钩子数组
queuePostRenderEffect(um, parentSuspense) // 加入后处理队列
}
// 8. Suspense相关处理
if (__FEATURE_SUSPENSE__ && parentSuspense && ...) {
parentSuspense.deps-- // 减少Suspense依赖计数
if (parentSuspense.deps === 0) {
parentSuspense.resolve() // 若所有依赖完成则resolve
}
}
}