Appearance
第2.1节:vnode 到真实 DOM 是如何转变的?
在 Vue.js 3 中,虚拟节点(Virtual Node,简称 vnode)到真实 DOM 的转换是整个渲染系统的核心。本节将深入分析这一过程的实现原理,从 vnode 的创建到最终的 DOM 操作,揭示 Vue 3 高效渲染机制的奥秘。
2.1.1 虚拟节点(VNode)的结构与创建
VNode 的基本结构
在 Vue 3 中,VNode 是一个描述真实 DOM 节点的 JavaScript 对象。让我们先看看 VNode 的核心结构:
typescript
// core/packages/runtime-core/src/vnode.ts
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any },
> {
__v_isVNode: true
type: VNodeTypes
props: (VNodeProps & ExtraProps) | null
key: string | number | symbol | null
ref: VNodeNormalizedRef | null
children: VNodeNormalizedChildren
component: ComponentInternalInstance | null
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
// DOM 相关
el: HostNode | null
anchor: HostNode | null
target: HostElement | null
targetStart: HostNode | null
targetAnchor: HostNode | null
// 优化标记
shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null
// 应用上下文
appContext: AppContext | null
// 其他元数据
scopeId: string | null
slotScopeIds: string[] | null
ssrId: number | null
memo: any[] | null
isDehydrated?: boolean
suspense?: SuspenseBoundary | null
ssrOptimized?: boolean
ctx?: ComponentInternalInstance | null
ce?: (instance: ComponentInternalInstance) => void
}VNode 的创建过程
1. createVNode 函数
createVNode 是创建虚拟节点的主要入口函数:
typescript
// core/packages/runtime-core/src/vnode.ts
export const createVNode = (
__DEV__ ? createVNodeWithArgsTransform : _createVNode
) as typeof _createVNode
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false,
): VNode {
// 类型校验和归一化
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
// 如果已经是 vnode,则克隆
if (isVNode(type)) {
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag = patchFlag | PatchFlags.BAIL
return cloned
}
// 类组件归一化
if (isClassComponent(type)) {
type = type.__vccOpts
}
// props 归一化
if (props) {
props = guardReactiveProps(props)!
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// 编码 vnode 类型信息
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true,
)
}2. createBaseVNode 函数
createBaseVNode 负责创建基础的 VNode 对象:
typescript
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false,
) {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssrId: null,
ssrOptimized: false,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetStart: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
ctx: currentRenderingInstance,
memo: null,
isDehydrated: false,
ce: null,
} as VNode
// 子节点归一化
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children)
// 归一化 suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).normalize(vnode)
}
} else if (children) {
// 编译生成的 vnode 接收到的 children 已经归一化
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
// 验证 key
if (__DEV__ && vnode.key !== vnode.key) {
warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
// 跟踪 vnode 用于块树优化
if (
isBlockTreeEnabled > 0 &&
// 避免块节点跟踪自己
!isBlockNode &&
// 有当前父块
currentBlock &&
// vnode 需要 patch
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// 过滤掉静态节点
vnode.patchFlag !== PatchFlags.NEED_HYDRATION
) {
currentBlock.push(vnode)
}
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
defineLegacyVNodeProperties(vnode)
}
return vnode
}2.1.2 Patch 算法:新旧 VNode 的对比与更新
Patch 函数的核心逻辑
patch 函数是 Vue 3 渲染系统的核心,负责对比新旧 VNode 并更新 DOM:
typescript
// core/packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
) => {
// 如果新旧节点相同,直接返回
if (n1 === n2) {
return
}
// 如果新旧节点类型不同,卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 如果有 BAIL 标记,关闭优化
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, namespace)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, namespace)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
break
default:
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
;(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})`)
}
}
// 设置 ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}元素节点的处理:processElement
对于普通的 HTML 元素,Vue 使用 processElement 函数进行处理:
typescript
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
if (n1 == null) {
// 挂载新元素
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
// 更新现有元素
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}2.1.3 组件的挂载与更新
组件处理:processComponent
组件的处理逻辑相对复杂,需要考虑组件实例的创建、初始化和更新:
typescript
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
// 挂载新组件
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// KeepAlive 组件的激活
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
namespace,
optimized,
)
} else {
// 普通组件挂载
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
optimized,
)
}
} else {
// 更新现有组件
updateComponent(n1, n2, optimized)
}
}组件挂载:mountComponent
mountComponent 函数负责挂载新的组件实例:
typescript
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
// 2.x 兼容模式
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`)
}
// 注入 renderer internals 用于 KeepAlive
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// 解析 props 和 slots 用于 setup() 上下文
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
// 设置异步依赖的 suspense
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect, optimized)
// 给异步组件一个占位符
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment, null, 'async'))
processCommentNode(null, placeholder, container, anchor)
}
return
}
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace,
optimized,
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}组件实例创建:createComponentInstance
组件实例是组件运行时的核心数据结构:
typescript
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null,
): ComponentInternalInstance {
const type = vnode.type as ConcreteComponent
// 继承父组件的应用上下文,或者从根 vnode 采用
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
appContext,
root: null!, // 将立即设置
next: null,
subTree: null!, // 将在创建后同步设置
effect: null!,
update: null!, // 将在创建后同步设置
job: null!,
scope: new EffectScope(true /* detached */),
render: null,
proxy: null,
exposed: null,
exposeProxy: null,
withProxy: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
ids: parent ? parent.ids : ['', 0, 0],
accessCache: null!,
renderCache: [],
// 本地解析的资源
components: null,
directives: null,
// 解析的 props 和 emits 选项
propsOptions: normalizePropsOptions(type, appContext),
emitsOptions: normalizeEmitsOptions(type, appContext),
// emit
emit: null!, // 将立即设置
emitted: null,
// props 默认值
propsDefaults: EMPTY_OBJ,
// inheritAttrs
inheritAttrs: type.inheritAttrs,
// 状态
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
setupContext: null,
// suspense 相关
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
asyncDep: null,
asyncResolved: false,
// 生命周期钩子
isMounted: false,
isUnmounted: false,
isDeactivated: false,
bc: null,
c: null,
bm: null,
m: null,
bu: null,
u: null,
um: null,
bum: null,
da: null,
a: null,
rtg: null,
rtc: null,
ec: null,
sp: null,
}
if (__DEV__) {
instance.ctx = createDevRenderContext(instance)
} else {
instance.ctx = { _: instance }
}
instance.root = parent ? parent.root : instance
instance.emit = emit.bind(null, instance)
// 应用自定义元素特殊处理
if (vnode.ce) {
vnode.ce(instance)
}
return instance
}组件初始化:setupComponent
setupComponent 函数负责初始化组件实例:
typescript
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false,
optimized = false,
): Promise<void> | undefined {
isSSR && setInSSRSetupState(isSSR)
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children, optimized || isSSR)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isSSR && setInSSRSetupState(false)
return setupResult
}
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean,
) {
const Component = instance.type as ComponentOptions
if (__DEV__) {
if (Component.name) {
validateComponentName(Component.name, instance.appContext.config)
}
if (Component.components) {
const names = Object.keys(Component.components)
for (let i = 0; i < names.length; i++) {
validateComponentName(names[i], instance.appContext.config)
}
}
if (Component.directives) {
const names = Object.keys(Component.directives)
for (let i = 0; i < names.length; i++) {
validateDirectiveName(names[i])
}
}
if (Component.compilerOptions && isRuntimeOnly()) {
warn(
`"compilerOptions" is only supported when using a build of Vue that ` +
`includes the runtime compiler. Since you are using a runtime-only ` +
`build, the options should be passed via your build tool config instead.`,
)
}
}
// 0. 创建渲染代理属性访问缓存
instance.accessCache = Object.create(null)
// 1. 创建公共实例 / 渲染代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. 调用 setup()
const { setup } = Component
if (setup) {
pauseTracking()
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
const reset = setCurrentInstance(instance)
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[
__DEV__ ? shallowReadonly(instance.props) : instance.props,
setupContext,
],
)
const isAsyncSetup = isPromise(setupResult)
resetTracking()
reset()
if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) {
// async setup / serverPrefetch,标记为异步边界用于 useId()
markAsyncBoundary(instance)
}
if (isAsyncSetup) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// 返回 promise 以便服务器渲染器可以等待它
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup 返回 Promise
// 在这里退出并等待重新进入
instance.asyncDep = setupResult
if (__DEV__ && !instance.suspense) {
const name = Component.name ?? 'Anonymous'
warn(
`Component <${name}>: setup function returned a promise, but no ` +
`<Suspense> boundary was found in the parent component tree. ` +
`A component with async setup() must be nested in a <Suspense> ` +
`in order to be rendered.`,
)
}
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`,
)
}
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}2.1.4 DOM 操作:nodeOps 的设计与实现
nodeOps 接口设计
Vue 3 通过 nodeOps 对象抽象了所有的 DOM 操作,使得渲染器可以跨平台工作:
typescript
// core/packages/runtime-dom/src/nodeOps.ts
const doc = (typeof document !== 'undefined' ? document : null) as Document
const templateContainer = doc && /*#__PURE__*/ doc.createElement('template')
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
// 插入节点
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
// 移除节点
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
// 创建元素
createElement: (tag, namespace, is, props): Element => {
const el =
namespace === 'svg'
? doc.createElementNS(svgNS, tag)
: namespace === 'mathml'
? doc.createElementNS(mathmlNS, tag)
: is
? doc.createElement(tag, { is })
: doc.createElement(tag)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
// 创建文本节点
createText: text => doc.createTextNode(text),
// 创建注释节点
createComment: text => doc.createComment(text),
// 设置文本内容
setText: (node, text) => {
node.nodeValue = text
},
// 设置元素文本内容
setElementText: (el, text) => {
el.textContent = text
},
// 获取父节点
parentNode: node => node.parentNode as Element | null,
// 获取下一个兄弟节点
nextSibling: node => node.nextSibling,
// 查询选择器
querySelector: selector => doc.querySelector(selector),
// 设置作用域 ID
setScopeId(el, id) {
el.setAttribute(id, '')
},
// 插入静态内容
insertStaticContent(content, parent, anchor, namespace, start, end) {
// 缓存插入
const beforeNode = anchor ? anchor.previousSibling : parent.lastChild
// 避免 innerHTML 时 leading whitespace 被移除
if (start && (start === end || start.nextSibling)) {
while (true) {
parent.insertBefore(start!.cloneNode(true), anchor)
if (start === end || !(start = start!.nextSibling)) break
}
} else {
// 新鲜插入
templateContainer.innerHTML =
namespace === 'svg'
? `<svg>${content}</svg>`
: namespace === 'mathml'
? `<math>${content}</math>`
: content
const template = templateContainer.content
if (namespace === 'svg' || namespace === 'mathml') {
// 移除外层的 svg/math 包装器
const wrapper = template.firstChild!
while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild)
}
template.removeChild(wrapper)
}
parent.insertBefore(template, anchor)
}
return [
// 第一个
beforeNode ? beforeNode.nextSibling! : parent.firstChild!,
// 最后一个
anchor ? anchor.previousSibling! : parent.lastChild!,
]
},
}DOM 操作的优化策略
- 批量操作:Vue 3 通过调度器将多个 DOM 操作合并到一个微任务中执行
- 静态提升:编译时将静态内容提升,避免重复创建
- Block Tree:通过块级优化减少不必要的遍历
- Patch Flags:精确标记需要更新的属性,避免全量对比
2.1.5 渲染流程时序图
2.1.6 性能优化要点
1. 编译时优化
- 静态提升:将静态节点提升到渲染函数外部
- Patch Flag 生成:编译时生成精确的更新标记
- Block Tree 优化:减少运行时的树遍历
2. 运行时优化
- 形状标记(Shape Flags):快速判断节点类型
- 动态子节点跟踪:只对比动态部分
- 组件更新优化:精确的组件更新判断
3. 内存优化
- 对象复用:复用 VNode 对象减少 GC 压力
- 弱引用:适当使用 WeakMap 避免内存泄漏
- 及时清理:组件卸载时清理相关引用
2.1.7 总结
Vue 3 的 vnode 到真实 DOM 的转换过程体现了现代前端框架的设计精髓:
- 分层设计:通过 VNode 抽象层将逻辑与平台解耦
- 精确更新:通过 Patch Flags 和 Block Tree 实现精确的差异更新
- 性能优化:编译时和运行时的双重优化策略
- 可扩展性:通过 nodeOps 抽象支持跨平台渲染
这套设计不仅保证了 Vue 3 的高性能,也为未来的扩展和优化奠定了坚实的基础。理解这一过程对于深入掌握 Vue 3 的工作原理和进行性能优化具有重要意义。
