虚拟 DOM 的实现原理
理解虚拟 DOM 的概念、作用及其在 Vue 中的实现方式
问题
什么是虚拟 DOM?如何实现一个虚拟 DOM?
解答
什么是虚拟 DOM
虚拟 DOM 是对真实 DOM 的抽象,用 JavaScript 对象(VNode 节点)来描述 DOM 树结构。一个虚拟 DOM 对象至少包含三个属性:标签名(tag)、属性(attrs)和子元素(children)。
以 Vue 为例,真实 DOM:
<div id="app">
<p class="p">节点内容</p>
<h3>{{ foo }}</h3>
</div>
对应的虚拟 DOM:
const app = new Vue({
el: "#app",
data: {
foo: "foo"
}
})
// render 函数生成的虚拟 DOM
(function anonymous() {
with(this) {
return _c('div', {attrs: {"id": "app"}}, [
_c('p', {staticClass: "p"}, [_v("节点内容")]),
_v(" "),
_c('h3', [_v(_s(foo))])
])
}
})
为什么需要虚拟 DOM
DOM 操作的性能开销很大。传统方式每次更新都会触发完整的渲染流程,如果连续更新 10 个 DOM 节点,浏览器会执行 10 次完整流程。
虚拟 DOM 的优势:
- 减少 DOM 操作:将多次更新合并,通过 diff 算法计算最小变更,一次性更新到真实 DOM
- 跨平台能力:抽象了渲染过程,可以渲染到不同平台(浏览器、Native、小程序等)
Vue 中的 VNode 结构
export default class VNode {
tag: string | void; // 标签名
data: VNodeData | void; // 节点数据
children: ?Array<VNode>; // 子节点
text: string | void; // 文本内容
elm: Node | void; // 对应的真实 DOM 节点
context: Component | void; // Vue 实例
key: string | number | void; // 节点的 key
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void;
parent: VNode | void;
isStatic: boolean; // 是否静态节点
isComment: boolean; // 是否注释节点
// ...
}
创建 VNode 的过程
Vue 通过 createElement 方法创建 VNode:
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 参数处理
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
核心实现在 _createElement 方法中:
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 规范化 children
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 内置标签,创建普通 VNode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 组件,通过 createComponent 创建
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
}
return vnode
}
对于组件类型,使用 createComponent 创建 VNode:
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// 构建子类构造函数
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// 安装组件钩子函数
installComponentHooks(data)
// 实例化 vnode
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
关键点
- 虚拟 DOM 是用 JavaScript 对象描述 DOM 树结构,包含 tag、data、children 等属性
- 通过 diff 算法计算最小变更,减少真实 DOM 操作次数,提升性能
- 抽象了渲染过程,实现跨平台能力(Web、Native、小程序等)
- Vue 通过 createElement 创建 VNode,会对 children 进行规范化处理
- 每个 VNode 的 children 也是 VNode,形成树形结构映射真实 DOM 树
目录