patch
// 用于将新的虚拟 DOM 节点更新到真实的 DOM 中
const patch: PatchFn = (
n1, // 旧节点
n2, // 新节点
container, // 目标容器
anchor = null, // 在哪个 DOM 节点之前插入新节点
parentComponent = null, // 新节点的父组件
parentSuspense = null, // 父悬挂节点
isSVG = false, // 是否在 SVG 环境中
slotScopeIds = null, // 插槽作用域 ID 数组
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // 是否启用了优化(静态提升、block 块缓存等)
) => {
// 新旧节点相同,则不往下走
if (n1 === n2) {
return
}
// 有旧节点,但节点类型不同,则卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 启用优化
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text: // 文本
processText(n1, n2, container, anchor)
break
case Comment: // 注释
processCommentNode(n1, n2, container, anchor)
break
case Static: // 静态资源
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment: // 分片
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理普通元素
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件元素
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理传送门
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理悬挂节点
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// 设置 ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
patch函数是 Vue3 中非常重要的函数,它用来对比新旧 VNode,更新 DOM,以实现数据的响应式渲染。其主要作用是根据新旧 VNode 的类型,选择不同的处理函数进行处理。以下是patch函数的主要处理流程:
- 判断新旧 VNode 是否相同,如果相同直接返回
- 判断新旧 VNode 的类型是否相同,如果不相同,则卸载旧节点,并用新节点进行替换
- 根据新旧 VNode 的类型,选择不同的处理函数进行处理,包括:文本、注释、静态资源、分片、元素、组件、传送门和悬挂节点
- 如果新 VNode 中定义了
ref,则设置对应的引用 - 如果新 VNode 中定义了动态子节点,标记为
optimized,启用优化 - 在执行完所有操作后,将新 VNode 赋值给容器的
_vnode属性
在这个过程中,processElement和processComponent函数是比较重要的,分别用于处理普通元素和组件元素的更新和渲染。
processComponent
const processComponent = (
n1: VNode | null, // 旧节点,如果是首次渲染则为 null。
n2: VNode, // 新节点,需要被挂载或更新的节点
container: RendererElement, // 目标容器
anchor: RendererNode | null, // 新节点插入位置的参照节点
parentComponent: ComponentInternalInstance | null, // 新节点的父组件实例
parentSuspense: SuspenseBoundary | null, // 父悬挂节点
isSVG: boolean, // 是否在 SVG 环境中
slotScopeIds: string[] | null, // 插槽作用域 ID 数组
optimized: boolean // 是否启用了优化(静态提升、block 块缓存等)
) => {
// 给新节点添加 插槽作用域 ID 数组
n2.slotScopeIds = slotScopeIds
// 首次渲染,传入 n1 是 null
if (n1 == null) {
// 激活缓存的子树节点
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// 挂载组件
// 1. 创建组件实例
// 2. 初始化组件实例
// 3. 建立组件更新机制
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// 更新组件
updateComponent(n1, n2, optimized)
}
}
processComponent 函数是处理组件类型的节点的函数,主要作用是判断组件节点是首次渲染还是更新,并分别调用 mountComponent 或 updateComponent 函数进行挂载或更新操作。
首先会给新节点 n2 添加插槽作用域 ID 数组,然后通过判断旧节点 n1 是否为空来判断是首次渲染还是更新。如果是首次渲染,如果新节点的 shapeFlag 中包含 ShapeFlags.COMPONENT_KEPT_ALIVE 则调用 activate 函数激活缓存的子树节点,否则调用 mountComponent 函数进行挂载。如果是更新操作,则调用 updateComponent 函数进行更新。
mountComponent
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
// 创建组件实例
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
if (__DEV__ && instance.type.__hmrId) {
registerHMR(instance)
}
if (__DEV__) {
pushWarningContext(initialVNode)
startMeasure(instance, `mount`)
}
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
// 2. 初始化组件实例
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
}
return
}
// 建立更新机制
// 获取vnode
// 1. 创建一个组件的更新函数
// 1.1 render 获得 vnode
// 1.2 patch(oldvnode,vnode)
// 2. 创建更新机制: new ReactiveEffect(更新函数)
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
mountComponent,用于初始化并挂载一个组件实例。函数中的主要步骤包括:
- 创建组件实例:根据传入的 VNode 创建一个组件实例。
- 初始化组件实例:对组件实例进行初始化,包括解析 props、slots 等。
- 建立更新机制:为组件实例创建一个更新函数,并通过
new ReactiveEffect建立更新机制。
函数执行完毕后,该组件实例就会被挂载到指定的容器中,并进行渲染。
总结
至此 Vue3 初始化流程就结束了,补全一些概念与重点函数
重点函数
- 应用实例:通过
createApp函数创建的应用实例是一个包含组件、指令、插件等功能的对象,可以调用其方法实现对应用的控制。 - 虚拟 DOM:Vue3 中的虚拟 DOM 是基于 VNode 的,VNode 是对真实 DOM 的抽象表示,可以通过
createVNode函数创建。 - 渲染函数:Vue3 通过渲染函数将 VNode 渲染为真实 DOM,渲染函数包括
patch函数和render函数,其中patch函数实现了虚拟 DOM 的比较和更新,render函数实现了将 VNode 渲染为真实 DOM 的过程。
重点函数
createApp: 创建一个应用实例,返回一个应用实例对象,包括提供了一些方法和选项,如mount、unmount、directive、component等。createRenderer: 创建一个渲染器实例,用于渲染组件或者 vnode。其中会创建一个 Patching 算法,用于对比新旧 vnode 的不同,并在必要时更新 DOM 树。mount: 用于挂载应用实例,其中会调用patch函数对组件进行初始化,并在必要时创建 DOM 节点。_createVNode: 用于创建 vnode 对象,其中会对 vnode 的 type、props、children 进行预处理,用于后续的渲染过程。patch: 用于对比新旧 vnode 的不同,并在必要时更新 DOM 树normalizeChildren: 用于规范化子节点,将子节点统一处理成一个数组,并对其中的字符串节点进行处理。