什么是 DOM DIFF
DOM DIFF 指的是针对两个(通常是虚拟)DOM 树结构进行“差异比对”的算法。在 react 中,进行比较的则是 fiber 架构。其目标有两个:
- 比较前后两棵树的节点差异
- 生成最小化的增删改操作序列,以便高效地把真实浏览器 DOM 从旧状态“打补丁”更新到新状态(复用节点、移动节点、删除节点)
这里讨论的是新的 fiber 节点为单节点的情况。
如何比较
DOM DIFF 规则:
- 同级比较,不跨级比较
- key 为唯一索引
- 批量更新
数据结构:
上图为需要比较的新旧 fiber 结构,目标为找到相同的 fiber 结构进行复用。相同的条件为 key 相等,type 相等。
流程图:
根据这个流程图,结合之前的 beginWork 阶段,在触发更新重新渲染页面时,可以对比 DIFF 子节点。
/**
*
* @param {*} returnFiber 父 fiber
* @param {*} currentFirstChild 老 fiber
* @param {*} element 新的虚拟 DOM
*/
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
// 新虚拟 DOM 的 key
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// 判断老 fiber 和新虚拟 DOM 的 key 是否相等
if (child.key === key) {
// 判断老 fiber 和新虚拟 DOM 的类型是否相等
if (child.type === element.type) {
deleteRemainingChildren(returnFiber, child.sibling);
// 如果都一样,则可以复用
const existing = useFiber(child, element.props);
existing.return = returnFiber;
return existing;
} else {
// 如果类型不同,则删除老的,创建新的
deleteRemainingChildren(returnFiber, child);
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 实现初次挂载
const created = createFiberFromElement(element);
created.return = returnFiber;
return created;
}
function deleteChild(returnFiber, childToDelete) {
if (!shouldTrackSideEffects) {
return;
}
// 删除的 fiber
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
function deleteRemainingChildren(returnFiber, currentFirstChild) {
if (!shouldTrackSideEffects) {
return;
}
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
}
在 deleteChild 中出现了一个字段 deletions,这是在当前父 fiber 节点中记录需要删除的子 fiber 节点,在提交阶段统一删除。
提交阶段代码:
/**
* 提交删除副作用
* @param {*} root 根节点
* @param {*} returnFiber 父 fiber
* @param {*} deletions 要删除的 fiber
*/
function commitDeletionEffects(root, returnFiber, deletedFiber) {
let parent = returnFiber;
// 找到真实 DOM 节点
findParent: while (parent !== null) {
switch (parent.tag) {
case HostRoot:
hostParent = parent.stateNode.containerInfo;
break findParent;
case HostComponent:
hostParent = parent.stateNode;
break findParent;
}
parent = parent.return;
}
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
hostParent = null;
}
function commitDeletionEffectsOnFiber(finishedRoot, nestedMountedAncestor, deletedFiber) {
switch (deletedFiber.tag) {
case HostComponent:
case HostText: {
// 删除文本节点
recursivelyTraverseDeletionEffects(finishedRoot, nestedMountedAncestor, deletedFiber);
if (hostParent !== null) {
removeChild(hostParent, deletedFiber.stateNode);
}
break;
}
}
}
function recursivelyTraverseDeletionEffects(finishedRoot, nestedMountedAncestor, parent) {
let child = parent.child;
while (child !== null) {
commitDeletionEffects(finishedRoot, nestedMountedAncestor, child);
child = child.sibling;
}
}
/**
* 递归遍历 Fiber 树,执行 Fiber 节点的副作用
* @param {*} parentFiber fiber 节点
* @param {*} root 根节点
*/
function recursivelyTraverseMutationEffects(parentFiber, root) {
// 先把父节点上该删除的节点都删除
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
commitDeletionEffects(root, parentFiber, childToDelete);
}
}
// 再去处理剩下的子节点
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child) {
commitMutationEffectsOnFiber(child, root);
child = child.sibling;
}
}
}
在提交阶段中需要注意,这时的操作都是针对真实 DOM 的,所以删除时需要找到真实 DOM 父节点,以调用 DOM 方法删除。且不能直接删除该节点,需递归删除子节点,以便后续支持声明周期。