经过前几篇文章的深入探索,我们完整地构建了 Vue3 的响应式系统。但响应式数据最终要渲染到页面上,这中间的桥梁就是虚拟DOM。今天,我们将深入 Vue3 虚拟 DOM 的设计与实现,看看它如何为高效的页面更新奠定基础。
前言:为什么需要虚拟DOM?
在传统的 jQuery 时代,我们直接操作真实 DOM:
$('#app').html('<div>Hello World</div>');
这种方式虽然直观,但有几个致命问题:
- 性能开销大:DOM 操作是浏览器中最昂贵的操作之一,频繁的 DOM 操作会严重影响系统性能
- 难以追踪:复杂应用的状态变化难以管理
- 手动操作:开发者需要手动维护 DOM 与状态的一致性
虚拟 DOM 的出现解决了这些问题:
// 虚拟 DOM 描述
const vnode = {
type: 'div',
props: { class: 'container' },
children: 'Hello World'
};
// 渲染器将虚拟 DOM 转换为真实 DOM
render(vnode, document.getElementById('app'));
虚拟 DOM 的本质:用 JavaScript 对象来描述真实 DOM 结构,通过比较新旧虚拟 DOM 的差异(diff),最小化地更新真实 DOM。
注:虚拟 DOM 相比真实 DOM 的优势在于:频繁操作 DOM 时,虚拟 DOM 可以先将操作收集,再一次性转成真实 DOM,渲染到页面上;而不需要每次操作都修改真实 DOM。
注:虚拟 DOM 不一定比真实 DOM 快,毕竟没有什么操作的性能能比 document.createElement('div') 更优了!
虚拟 DOM 的结构变化
Vue2 的 VNode 结构
// Vue2 的 VNode 结构(简化)
interface VNode {
tag?: string; // 标签名
data?: VNodeData; // 属性、事件等
children?: VNode[]; // 子节点
text?: string; // 文本内容
elm?: Node; // 对应的真实 DOM
key?: string | number; // 唯一标识
// ... 其他属性
}
Vue3 的 VNode 结构
// Vue3 的 VNode 结构(简化)
interface VNode {
__v_isVNode: true; // 标记为 VNode
type: any; // 类型:元素标签、组件、Fragment等
props: any; // 属性
children: any; // 子节点
shapeFlag: number; // 节点类型标志(位掩码)
patchFlag: number; // 优化标志(位掩码)
dynamicProps: string[] | null; // 动态属性列表
staticCount: number; // 静态节点计数
key: any; // 唯一标识
ref: any; // 引用
el: HostNode | null; // 真实 DOM 节点
anchor: HostNode | null; // 锚点(Fragment 使用)
// 组件相关
component: any; // 组件实例
suspense: any; // Suspense 相关
ssContent: any; // SSR 内容
ssFallback: any; // SSR 回退
// 优化相关
scopeId: string | null; // 作用域 ID
slotScopeIds: string[] | null; // 插槽作用域 ID
}
Vue3 VNode 结构的主要变化
1. 更明确的类型标识
type: 'div' | 'span' | MyComponent | Fragment | Text | Comment | Static;
2. 使用 shapeFlag 位掩码标记类型
const enum ShapeFlags {
ELEMENT = 1, // 元素节点
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 状态组件
TEXT_CHILDREN = 1 << 3, // 文本子节点
ARRAY_CHILDREN = 1 << 4, // 数组子节点
SLOTS_CHILDREN = 1 << 5, // 插槽子节点
TELEPORT = 1 << 6, // Teleport
SUSPENSE = 1 << 7, // Suspense
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9
}
3. 使用 patchFlag 标记动态内容
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态属性
FULL_PROPS = 1 << 4, // 全量比较
HYDRATE_EVENTS = 1 << 5, // 事件监听
STABLE_FRAGMENT = 1 << 6, // 稳定 Fragment
KEYED_FRAGMENT = 1 << 7, // 带 key 的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 无 key 的 Fragment
NEED_PATCH = 1 << 9, // 需要非 props 比较
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
DEV_ROOT_FRAGMENT = 1 << 11, // 开发环境根 Fragment
// 特殊标志
HOISTED = -1, // 静态提升节点
BAIL = -2 // 退出优化
}
VNode 的核心属性
1. type:节点类型
元素节点
const elementVNode = {
type: 'div',
props: { class: 'box' },
children: 'Hello'
};
组件节点
const MyComponent = {
setup() {
return () => h('div', '组件内容');
}
};
const componentVNode = {
type: MyComponent,
props: { title: '标题' }
};
文本节点
const textVNode = {
type: Text,
props: null,
children: '文本内容'
};
Fragment(片段)
const fragmentVNode = {
type: Fragment,
children: [
h('div', '子节点1'),
h('div', '子节点2')
]
};
静态节点
const staticVNode = {
type: 'div',
props: { class: 'static' },
children: '静态内容',
patchFlag: PatchFlags.HOISTED // 标记为提升
};
2. props:属性
function createVNode(type, props, children) {
const vnode = {
type,
props: props || {},
children,
// 提取关键属性
key: props && props.key,
ref: props && props.ref,
// 清理 props 中的特殊属性
...normalizeProps(props)
};
return vnode;
}
function normalizeProps(props) {
if (!props) return {};
// 分离特殊属性
const { key, ref, ...pureProps } = props;
return {
props: pureProps,
key,
ref
};
}
3. children:子节点
文本子节点
const vnode1 = {
type: 'div',
children: '纯文本',
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
};
数组子节点
const vnode2 = {
type: 'div',
children: [
h('span', '子节点1'),
h('span', '子节点2')
],
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
};
插槽子节点
const vnode3 = {
type: MyComponent,
children: {
default: () => h('div', '默认插槽'),
header: () => h('div', '头部插槽')
},
shapeFlag: ShapeFlags.COMPONENT | ShapeFlags.SLOTS_CHILDREN
};
空子节点
const vnode4 = {
type: 'div',
children: null,
shapeFlag: ShapeFlags.ELEMENT
};
多种 VNode 类型
元素节点
function createElementVNode(tag, props, children) {
const vnode = {
type: tag,
props,
children,
shapeFlag: ShapeFlags.ELEMENT
};
// 设置子节点类型标志
if (typeof children === 'string') {
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
} else if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
}
return vnode;
}
组件节点
function createComponentVNode(component, props, children) {
const vnode = {
type: component,
props,
children,
shapeFlag: ShapeFlags.STATEFUL_COMPONENT
};
// 处理插槽
if (typeof children === 'object') {
vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
}
// 组件实例(稍后填充)
vnode.component = null;
return vnode;
}
文本节点
const Text = Symbol('Text');
function createTextVNode(text) {
return {
type: Text,
props: null,
children: String(text),
shapeFlag: ShapeFlags.TEXT_CHILDREN
};
}
Fragment 节点
const Fragment = Symbol('Fragment');
function createFragmentVNode(children) {
return {
type: Fragment,
props: null,
children,
shapeFlag: Array.isArray(children)
? ShapeFlags.ARRAY_CHILDREN
: ShapeFlags.TEXT_CHILDREN
};
}
静态节点
function createStaticVNode(content, count) {
return {
type: 'div',
props: null,
children: content,
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
patchFlag: PatchFlags.HOISTED,
staticCount: count
};
}
静态提升(Static Hoisting)
静态提升的原理
我们先来看一段模版代码:
<div>
<span>静态文本</span>
<span>{{ dynamic }}</span>
</div>
没有静态提升下的渲染函数:
function render(ctx) {
return h('div', [
h('span', '静态文本'), // 每次渲染都创建
h('span', ctx.dynamic)
]);
}
没有静态提升下,对于 <span>静态文本</span> 这段代码,每次渲染时都会创建。
静态提升下的渲染函数:
const _hoisted_1 = h('span', '静态文本'); // 提升到函数外
function render(ctx) {
return h('div', [
_hoisted_1, // 直接复用
h('span', ctx.dynamic)
]);
}
静态提升下,对于 <span>静态文本</span> 这段代码,会将静态文本的 VNode 提升到函数外,在需要的时候直接复用即可!
实现静态提升
// 编译器生成的代码示例
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
// 静态节点提升
const _hoisted_1 = _createVNode("span", null, "静态文本", PatchFlags.HOISTED)
const _hoisted_2 = _createVNode("div", { class: "static-class" }, [
_hoisted_1,
_createVNode("span", null, "另一个静态节点", PatchFlags.HOISTED)
], PatchFlags.HOISTED)
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_2, // 直接使用提升的节点
_createVNode("span", null, _ctx.dynamic, PatchFlags.TEXT)
]))
}
Patch Flags 的作用
为什么要用 Patch Flags?
无 Patch Flags:需要全量比较:
function patch(oldVNode, newVNode) {
// 比较所有属性
if (oldVNode.props.class !== newVNode.props.class) {
updateClass();
}
if (oldVNode.props.style !== newVNode.props.style) {
updateStyle();
}
if (oldVNode.props.id !== newVNode.props.id) {
updateId();
}
// ... 比较所有可能的属性
}
有 Patch Flags:只比较动态部分:
function patch(oldVNode, newVNode) {
if (newVNode.patchFlag & PatchFlags.CLASS) {
// 只有 class 是动态的
updateClass();
}
if (newVNode.patchFlag & PatchFlags.STYLE) {
// 只有 style 是动态的
updateStyle();
}
// 只比较标记为动态的属性
}
Patch Flags 的实现
// 动态节点标记
function createVNodeWithFlags(type, props, children, flag) {
const vnode = createVNode(type, props, children);
vnode.patchFlag = flag;
// 记录动态属性名
if (flag & PatchFlags.PROPS) {
vnode.dynamicProps = Object.keys(props).filter(
key => !isStaticProperty(key)
);
}
return vnode;
}
// 使用示例
const dynamicClassVNode = createVNodeWithFlags(
'div',
{ class: dynamicClass }, // class 动态
'内容',
PatchFlags.CLASS
);
const dynamicTextVNode = createVNodeWithFlags(
'span',
null,
dynamicText,
PatchFlags.TEXT
);
const multipleDynamicsVNode = createVNodeWithFlags(
'div',
{
class: dynamicClass,
style: dynamicStyle,
id: 'static-id' // 静态属性
},
'内容',
PatchFlags.CLASS | PatchFlags.STYLE
);
// dynamicProps: ['class', 'style']
h 函数的实现
h 函数的基本实现
/**
* h 函数:创建 VNode 的辅助函数
* @param {string|object} type - 节点类型
* @param {object} props - 属性
* @param {array|string} children - 子节点
* @returns {object} VNode
*/
function h(type, props, children) {
// 处理参数重载
const args = normalizeArgs(type, props, children);
return createVNode(args.type, args.props, args.children);
}
function normalizeArgs(type, props, children) {
// 如果没有 props
if (arguments.length === 2) {
if (isObject(props) && !isArray(props)) {
// h('div', { class: 'box' })
return { type, props, children: null };
} else {
// h('div', '文本内容')
return { type, props: null, children: props };
}
}
// 完整参数
return { type, props, children };
}
function isObject(val) {
return val !== null && typeof val === 'object';
}
function isArray(val) {
return Array.isArray(val);
}
完整的 createVNode 实现
/**
* 创建 VNode
* @param {any} type - 节点类型
* @param {object} props - 属性
* @param {any} children - 子节点
* @param {number} patchFlag - 优化标志
* @param {object} dynamicProps - 动态属性列表
* @returns {object} VNode
*/
function createVNode(type, props, children, patchFlag, dynamicProps) {
// 处理 props
props = normalizeProps(props);
// 提取 key 和 ref
const { key, ref } = props || {};
// 计算 shapeFlag
const shapeFlag = getShapeFlag(type, children);
// 创建基础 VNode
const vnode = {
__v_isVNode: true,
type,
props: props || null,
children,
shapeFlag,
// 优化相关
patchFlag: patchFlag || 0,
dynamicProps: dynamicProps || null,
// 核心属性
key,
ref,
// 运行时相关
el: null, // 真实 DOM
anchor: null, // 锚点(Fragment)
component: null, // 组件实例
parent: null, // 父 VNode
// 其他
scopeId: null,
slotScopeIds: null
};
// 处理子节点
normalizeChildren(vnode, children);
// 如果有动态 children,记录
if (shouldTrackDynamicChildren(vnode)) {
vnode.dynamicChildren = [];
}
return vnode;
}
function normalizeProps(props) {
if (!props) return null;
// 移除 Vue 内部使用的特殊属性
const { class: klass, style, ...rest } = props;
// 合并 class
if (klass) {
rest.class = normalizeClass(klass);
}
// 合并 style
if (style) {
rest.style = normalizeStyle(style);
}
return rest;
}
function getShapeFlag(type, children) {
let shapeFlag = 0;
// 判断类型
if (typeof type === 'string') {
shapeFlag = ShapeFlags.ELEMENT;
} else if (type === Text) {
shapeFlag = ShapeFlags.TEXT_CHILDREN;
} else if (type === Fragment) {
shapeFlag = ShapeFlags.FRAGMENT;
} else {
shapeFlag = ShapeFlags.STATEFUL_COMPONENT;
}
// 判断子节点类型
if (children) {
if (typeof children === 'string') {
shapeFlag |= ShapeFlags.TEXT_CHILDREN;
} else if (Array.isArray(children)) {
shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
} else if (isObject(children)) {
shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
}
}
return shapeFlag;
}
function normalizeChildren(vnode, children) {
if (!children) return;
// 标准化文本子节点
if (typeof children === 'string' || typeof children === 'number') {
vnode.children = String(children);
}
// 标准化数组子节点
if (Array.isArray(children)) {
vnode.children = children.map(child => {
if (typeof child === 'string') {
return createTextVNode(child);
}
return child;
});
}
}
function shouldTrackDynamicChildren(vnode) {
return vnode.patchFlag > 0 ||
vnode.patchFlag === PatchFlags.HOISTED ||
vnode.shapeFlag & ShapeFlags.COMPONENT;
}
// 工具函数:规范化 class
function normalizeClass(value) {
if (typeof value === 'string') return value;
if (Array.isArray(value)) {
return value.map(normalizeClass).filter(Boolean).join(' ');
}
if (isObject(value)) {
return Object.keys(value)
.filter(key => value[key])
.join(' ');
}
return '';
}
// 工具函数:规范化 style
function normalizeStyle(value) {
if (typeof value === 'string') return value;
if (Array.isArray(value)) {
return Object.assign({}, ...value.map(normalizeStyle));
}
if (isObject(value)) return value;
return {};
}
h 函数的完整版本
/**
* 完整的 h 函数实现
* 支持多种调用方式:
* h('div')
* h('div', { class: 'box' })
* h('div', '文本')
* h('div', {}, ['子节点1', '子节点2'])
* h(Component, { props })
*/
function h(type, propsOrChildren, children) {
const args = arguments.length;
// h('div')
if (args === 1) {
return createVNode(type, null, null);
}
// h('div', {})
if (args === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// 第二个参数是 props
return createVNode(type, propsOrChildren, null);
} else {
// 第二个参数是 children
return createVNode(type, null, propsOrChildren);
}
}
// h('div', {}, '文本')
// h('div', {}, [])
// h('div', {}, h('span'))
if (args === 3) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// 有 props
return createVNode(type, propsOrChildren, children);
} else {
// 无 props
return createVNode(type, null, propsOrChildren);
}
}
// 更多参数(不常见)
const props = propsOrChildren;
const _children = Array.from(arguments).slice(2);
return createVNode(type, props, _children);
}
实战:使用 h 函数创建组件
// 定义组件
const MyComponent = {
setup(props) {
const count = ref(0);
return () => h('div', { class: 'counter' }, [
h('h3', props.title),
h('p', `计数: ${count.value}`),
h('button', {
onClick: () => count.value++
}, '增加')
]);
}
};
// 创建 VNode
const vnode = h(MyComponent, {
title: '我的计数器'
});
// 模拟渲染
function render(vnode, container) {
if (typeof vnode.type === 'object') {
// 组件
const component = vnode.type;
const subTree = component.setup(vnode.props);
render(subTree, container);
} else if (typeof vnode.type === 'string') {
// 元素
const el = document.createElement(vnode.type);
// 设置属性
if (vnode.props) {
Object.entries(vnode.props).forEach(([key, value]) => {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), value);
} else {
el.setAttribute(key, value);
}
});
}
// 处理子节点
if (typeof vnode.children === 'string') {
el.textContent = vnode.children;
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => render(child, el));
}
container.appendChild(el);
vnode.el = el;
}
}
// 挂载
render(vnode, document.getElementById('app'));
结语
Vue3 的虚拟 DOM 在设计上进行了大量的优化,理解虚拟 DOM 的设计与实现,不仅帮助我们写出更高效的 Vue 应用,也为后续学习 diff 算法和渲染器打下坚实基础。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!