虚拟DOM与Diff算法深度解析—Vue双端Diff与React Fiber时间切片|新宇宙博客
Back to list虚拟 DOM 与 Diff 算法
Site Owner
Published on 2026-05-21
详解虚拟DOM设计权衡、双端Diff算法、最长递增子序列优化、React Fiber时间切片及key属性的正确使用
虚拟 DOM 与 Diff 算法
虚拟 DOM(VNode)的数据结构设计;同层 Diff 的核心假设与 O(n) 算法;key 在列表 Diff 中的关键作用;Vue 3 的最长递增子序列(LIS)优化;React Fiber 的可中断调度架构;以及 patchFlag 静态提升等编译期优化手段。
目录
虚拟 DOM 的本质
VNode 数据结构
Diff 算法基础
有 key 的列表 Diff
最长递增子序列(LIS)优化
React Fiber 架构
编译期优化
实战案例
深度追问
虚拟 DOM 的本质
虚拟 DOM 不是"更快",而是可预测性与跨平台能力 的权衡:
真实 DOM 操作成本
├── DOM 创建/删除 → 高
├── 样式重计算 → 高
├── Layout(回流) → 极高
└── 内存分配 → 高
VNode 操作成本
├── JS 对象创建 → 低
├── 对象比较(diff) → 中
└── 批量 DOM 更新 → 通过调度优化
核心价值:
最小化 DOM 操作 :将多次状态变更合并为一次 patch
跨平台 :VNode 可渲染到 DOM、Native(RN)、Canvas(Pixi.js)、SSR
可缓存性 :静态 VNode 提升(hoisting),避免重复创建
VNode 数据结构
const VNodeTypes = {
TEXT : Symbol ('Text' ),
COMMENT : Symbol ('Comment' ),
FRAGMENT : Symbol ('Fragment' ),
ELEMENT : 'element' ,
COMPONENT : 'component'
};
const PatchFlags = {
TEXT : 1 ,
CLASS : 2 ,
STYLE : 4 ,
PROPS : 8 ,
FULL_PROPS : 16 ,
STABLE_FRAGMENT : 64 ,
KEYED_FRAGMENT : 128 ,
UNKEYED_FRAGMENT : 256 ,
NEED_PATCH : 512 ,
DYNAMIC_SLOTS : 1024 ,
HOISTED : -1 ,
BAIL : -2
};
function createVNode (type, props = null , children = null , patchFlag = 0 , shapeFlag = 0 ) {
return {
__v_isVNode : true ,
type,
props,
key : props?.key ?? null ,
ref : props?.ref ?? null ,
children,
component : null ,
el : null ,
anchor : null ,
patchFlag,
shapeFlag,
dynamicProps : null ,
dynamicChildren : null ,
};
}
function h (type, props, children ) {
let shapeFlag = 0 ;
if (typeof type === 'string' ) {
shapeFlag = 1 ;
} else if (typeof type === 'object' ) {
shapeFlag = 4 ;
}
return createVNode (type, props, children, 0 , shapeFlag);
}
Diff 算法基础
三大核心假设(同层对比) 1. 不同类型的节点 → 直接销毁重建(不尝试复用)
2. 相同 key 的节点 → 认为是同一节点,尝试复用
3. 只对同层节点进行比较(不跨层级)
patch 入口 function patch (n1, n2, container, anchor = null ) {
if (n1 === n2) return ;
if (n1 && !isSameVNodeType (n1, n2)) {
unmount (n1);
n1 = null ;
}
const { type, shapeFlag } = n2;
switch (type) {
case VNodeTypes .TEXT :
processText (n1, n2, container);
break ;
case VNodeTypes .FRAGMENT :
processFragment (n1, n2, container);
break ;
default :
if (shapeFlag & 1 ) {
processElement (n1, n2, container, anchor);
} else if (shapeFlag & 4 ) {
processComponent (n1, n2, container);
}
}
}
function isSameVNodeType (n1, n2 ) {
return n1.type === n2.type && n1.key === n2.key ;
}
function patchElement (n1, n2 ) {
const el = (n2.el = n1.el );
const oldProps = n1.props ?? {};
const newProps = n2.props ?? {};
if (n2.patchFlag > 0 ) {
if (n2.patchFlag & PatchFlags .CLASS ) {
if (oldProps.class !== newProps.class ) {
el.className = newProps.class ?? '' ;
}
}
if (n2.patchFlag & PatchFlags .STYLE ) {
patchStyle (el, oldProps.style , newProps.style );
}
if (n2.patchFlag & PatchFlags .TEXT ) {
if (n1.children !== n2.children ) {
el.textContent = n2.children ;
}
}
if (n2.patchFlag & PatchFlags .PROPS ) {
for (const key of n2.dynamicProps ) {
patchProp (el, key, oldProps[key], newProps[key]);
}
}
} else {
patchProps (el, n2, oldProps, newProps);
}
patchChildren (n1, n2, el);
}
有 key 的列表 Diff
Vue 3 的五步 Diff 算法 function patchKeyedChildren (c1, c2, container ) {
let i = 0 ;
const l2 = c2.length ;
let e1 = c1.length - 1 ;
let e2 = l2 - 1 ;
while (i <= e1 && i <= e2) {
if (isSameVNodeType (c1[i], c2[i])) {
patch (c1[i], c2[i], container);
i++;
} else break ;
}
while (i <= e1 && i <= e2) {
if (isSameVNodeType (c1[e1], c2[e2])) {
patch (c1[e1], c2[e2], container);
e1--; e2--;
} else break ;
}
if (i > e1) {
if (i <= e2) {
const anchor = e2 + 1 < l2 ? c2[e2 + 1 ].el : null ;
while (i <= e2) {
mount (c2[i++], container, anchor);
}
}
}
else if (i > e2) {
while (i <= e1) unmount (c1[i++]);
}
else {
const s1 = i;
const s2 = i;
const keyToNewIndexMap = new Map ();
for (let j = s2; j <= e2; j++) {
const key = c2[j].key ;
if (key != null ) keyToNewIndexMap.set (key, j);
}
let patched = 0 ;
const toBePatched = e2 - s2 + 1 ;
let moved = false ;
let maxNewIndexSoFar = 0 ;
const newIndexToOldIndexMap = new Array (toBePatched).fill (0 );
for (let j = s1; j <= e1; j++) {
const prevChild = c1[j];
if (patched >= toBePatched) {
unmount (prevChild);
continue ;
}
let newIndex;
if (prevChild.key != null ) {
newIndex = keyToNewIndexMap.get (prevChild.key );
} else {
for (let k = s2; k <= e2; k++) {
if (newIndexToOldIndexMap[k - s2] === 0 &&
isSameVNodeType (prevChild, c2[k])) {
newIndex = k;
break ;
}
}
}
if (newIndex === undefined ) {
unmount (prevChild);
} else {
newIndexToOldIndexMap[newIndex - s2] = j + 1 ;
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true ;
}
patch (prevChild, c2[newIndex], container);
patched++;
}
}
const increasingNewIndexSequence = moved
? getSequence (newIndexToOldIndexMap)
: [];
let j = increasingNewIndexSequence.length - 1 ;
for (let k = toBePatched - 1 ; k >= 0 ; k--) {
const nextIndex = s2 + k;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1 ].el : null ;
if (newIndexToOldIndexMap[k] === 0 ) {
mount (nextChild, container, anchor);
} else if (moved) {
if (j < 0 || k !== increasingNewIndexSequence[j]) {
move (nextChild, container, anchor);
} else {
j--;
}
}
}
}
}
最长递增子序列(LIS)优化
为什么需要 LIS? 旧节点:[a, b, c, d, e]
新节点:[a, d, b, c, e]
newIndexToOldIndexMap(乱序区间): [4, 2, 3]
d b c
LIS(最长递增子序列): [2, 3] (即 b, c 的位置)
→ b, c 不需要移动,只移动 d
没有 LIS:d, b, c 全部移动(3次 DOM 操作)
有 LIS:只移动 d(1次 DOM 操作)
function getSequence (arr ) {
const p = arr.slice ();
const result = [0 ];
for (let i = 1 ; i < arr.length ; i++) {
const arrI = arr[i];
if (arrI !== 0 ) {
const j = result[result.length - 1 ];
if (arr[j] < arrI) {
p[i] = j;
result.push (i);
continue ;
}
let left = 0 ;
let right = result.length - 1 ;
while (left < right) {
const mid = (left + right) >> 1 ;
if (arr[result[mid]] < arrI) {
left = mid + 1 ;
} else {
right = mid;
}
}
if (arrI < arr[result[left]]) {
if (left > 0 ) p[i] = result[left - 1 ];
result[left] = i;
}
}
}
let u = result.length ;
let v = result[u - 1 ];
while (u-- > 0 ) {
result[u] = v;
v = p[v];
}
return result;
}
console .log (getSequence ([4 , 2 , 3 ]));
React Fiber 架构
Fiber 节点结构
const FiberTypes = {
FunctionComponent : 0 ,
ClassComponent : 1 ,
HostRoot : 3 ,
HostElement : 5 ,
HostText : 6 ,
};
class FiberNode {
constructor (tag, pendingProps, key ) {
this .tag = tag;
this .key = key;
this .type = null ;
this .stateNode = null ;
this .return = null ;
this .child = null ;
this .sibling = null ;
this .index = 0 ;
this .pendingProps = pendingProps;
this .memoizedProps = null ;
this .memoizedState = null ;
this .updateQueue = null ;
this .flags = 0 ;
this .subtreeFlags = 0 ;
this .deletions = null ;
this .alternate = null ;
this .lanes = 0 ;
this .childLanes = 0 ;
}
}
const Flags = {
NoFlags : 0b0000000 ,
Placement : 0b0000010 ,
Update : 0b0000100 ,
Deletion : 0b0001000 ,
Passive : 0b1000000 ,
};
可中断渲染(时间切片)
let workInProgress = null ;
let shouldYield = false ;
function workLoopConcurrent ( ) {
while (workInProgress !== null && !shouldYield) {
performUnitOfWork (workInProgress);
shouldYield = scheduler.shouldYield ();
}
}
function performUnitOfWork (fiber ) {
const next = beginWork (fiber);
if (next === null ) {
completeUnitOfWork (fiber);
} else {
workInProgress = next;
}
}
function completeUnitOfWork (fiber ) {
let completedWork = fiber;
do {
completeWork (completedWork);
const sibling = completedWork.sibling ;
if (sibling) {
workInProgress = sibling;
return ;
}
completedWork = completedWork.return ;
} while (completedWork);
}
function commitRoot (root ) {
commitBeforeMutationEffects (root);
commitMutationEffects (root);
commitLayoutEffects (root);
schedulePassiveEffects (root);
}
编译期优化
Vue 3 的静态提升(Hoisting)
function render ( ) {
return h ('div' , null , [
h ('span' , { class : 'static' }, '静态内容' ),
h ('p' , null , dynamic.value )
]);
}
const _hoisted_1 = h ('span' , { class : 'static' }, '静态内容' );
function render ( ) {
return h ('div' , null , [
_hoisted_1,
h ('p' , { patchFlag : PatchFlags .TEXT }, dynamic.value )
]);
}
Block 与 dynamicChildren
const blockStack = [];
function openBlock ( ) {
blockStack.push ([]);
}
function createBlock (type, props, children ) {
const vnode = createVNode (type, props, children);
vnode.dynamicChildren = blockStack.pop ();
return vnode;
}
function createVNodeWithFlag (type, props, children, patchFlag ) {
const vnode = createVNode (type, props, children, patchFlag);
if (patchFlag > 0 && blockStack.length ) {
blockStack[blockStack.length - 1 ].push (vnode);
}
return vnode;
}
function patchBlock (n1, n2, container ) {
const dynamicChildren = n2.dynamicChildren ;
if (dynamicChildren) {
for (let i = 0 ; i < dynamicChildren.length ; i++) {
patchElement (n1.dynamicChildren [i], dynamicChildren[i]);
}
} else {
patchChildren (n1, n2, container);
}
}
实战案例
手写简版 reconciler
function createRenderer (options ) {
const { createElement, insert, setElementText, patchProp } = options;
function mount (vnode, container ) {
const el = (vnode.el = createElement (vnode.type ));
if (vnode.props ) {
for (const key in vnode.props ) {
patchProp (el, key, null , vnode.props [key]);
}
}
if (typeof vnode.children === 'string' ) {
setElementText (el, vnode.children );
} else if (Array .isArray (vnode.children )) {
vnode.children .forEach (child => mount (child, el));
}
insert (el, container);
}
function patch (n1, n2, container ) {
if (n1.type !== n2.type ) {
n1.el .remove ();
mount (n2, container);
return ;
}
const el = (n2.el = n1.el );
const oldProps = n1.props ?? {};
const newProps = n2.props ?? {};
for (const key in newProps) {
patchProp (el, key, oldProps[key], newProps[key]);
}
for (const key in oldProps) {
if (!(key in newProps)) patchProp (el, key, oldProps[key], null );
}
patchChildren (n1, n2, el);
}
return { mount, patch };
}
const domRenderer = createRenderer ({
createElement : tag => document .createElement (tag),
insert : (el, parent, anchor = null ) => parent.insertBefore (el, anchor),
setElementText : (el, text ) => el.textContent = text,
patchProp : (el, key, oldVal, newVal ) => {
if (key.startsWith ('on' )) {
const event = key.slice (2 ).toLowerCase ();
if (oldVal) el.removeEventListener (event, oldVal);
if (newVal) el.addEventListener (event, newVal);
} else if (newVal == null ) {
el.removeAttribute (key);
} else {
el.setAttribute (key, newVal);
}
}
});
深度追问 Q1:为什么 Virtual DOM 的 Diff 是 O(n) 而不是最优的 O(n³)?
最优 Tree Edit Distance 算法是 O(n³),对于有 1000 个节点的组件会产生 10 亿次操作。Vue/React 通过三个启发式假设将复杂度降到 O(n):①不跨层级复用;②不同类型直接替换;③用 key 标识节点身份。实践中这三个假设覆盖了 99% 的 UI 更新场景,是工程上的最优解。
Q2:key 的作用是什么?为什么不能用 index 作为 key?
key 是 Diff 算法识别节点身份的唯一凭据,相同 key 的节点会复用 DOM 和状态。用 index 作 key 的问题:当列表顺序改变(如头部插入、排序)时,原本的 key-index 映射被打乱,Diff 算法会错误地"复用"了不匹配的节点,导致状态残留和不必要的 DOM 操作。应使用稳定的业务 ID 作为 key。
Q3:Vue 3 的 LIS 优化比 Vue 2 的双端 Diff 好在哪里?
Vue 2 的双端 Diff 使用四个指针从两端向中间推进,时间复杂度 O(n),但在乱序场景下 DOM 移动次数不是最优的。Vue 3 的 LIS 算法确保最小化 DOM 移动次数 :LIS 中的节点不需要移动,只移动不在 LIS 中的节点。代价是额外的 O(n log n) LIS 计算,但减少了实际 DOM 操作,整体更快。
Q4:React Fiber 的"双缓冲"是什么意思?
React 维护两棵 Fiber 树:current 树 (当前屏幕显示的)和 workInProgress 树 (正在构建的)。渲染完成后两棵树通过 alternate 指针互换。好处:① 渲染过程中 current 树始终可读,不影响当前 UI;② 新树构建失败可直接丢弃;③ 节点复用(alternate 对象池),减少 GC 压力。
Q5:没有 key 时 Diff 算法如何处理列表?
无 key 时按位置匹配(index 对应),如果列表长度变化,多出的节点直接创建/删除。这在列表末尾增删时是最优的(无 DOM 移动),但在头部插入时会导致所有节点 patch,远不如带 key 的方案高效。