「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
react 版本:v17.0.3
深度优先遍历
深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点 v 的所在边都己被探寻过,搜索将回溯到发现节点 v 的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。
算法实现方式
深度优先遍历通常有递归和非递归两种实现方式。
递归实现
递归实现比较简单,表达性很好,也很容易理解,不过如果层级过深,很容易导致栈溢出。
function deepFirstSearch(node,nodeList) {
if (node) {
nodeList.push(node);
var children = node.children;
for (var i = 0; i < children.length; i++)
//每次递归的时候将 需要遍历的节点 和 节点所存储的数组传下去
deepFirstSearch(children[i],nodeList);
}
return nodeList;
}
非递归实现
非递归方式借助栈来模拟递归,可以不用担心递归那样层级过深导致的栈溢出问题。
function deepFirstSearch(node) {
var nodes = [];
if (node != null) {
var stack = [];
stack.push(node);
while (stack.length != 0) {
var item = stack.pop();
nodes.push(item);
var children = item.children;
for (var i = children.length - 1; i >= 0; i--)
stack.push(children[i]);
}
}
return nodes;
}
React中的应用场景
深度优先遍历在react中的使用有三个地方,分别是 fiber树的构造,查找 context 的消费节点,commit阶段DOM节点的删除。
fiber树的构造
fiber树的构造过程发生在 reconciler 协调过程中,源码位于 react-reconciler 包的 ReactFiberWorkLoop.new.js 文件中。我们先来回顾一下 reconciler 的过程 (具体详情可阅读《React源码解读之任务调度流程》一文):
reconciler 的过程可以分为四个阶段:
-
任务输入:scheduleUpdateOnFiber 是处理更新任务开始的地方
-
调度任务注册:与调度中心(Scheduler)交互,注册调度任务(scheduler task),等待回调
-
执行任务回调:执行渲染任务,在内存中构造出fiber树,同时与渲染器(react-dom)交互,在内存中创建出与fiber对应的DOM节点
-
输出DOM节点:与渲染器(react-dom)交互,渲染DOM节点
fiber树构造过程中深度优先遍历的使用就在执行任务回调阶段。入口函数是 ReactFiberWorkLoop.new.js 文件中的 workLoopSync()。
下面的代码做了精简处理,将于dfs无关的逻辑代码去掉了
function workLoopSync() {
// 1、最外层循环,保证每一个节点都能遍历到,不会遗漏
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
let next;
// 2、beginWork是向下探寻阶段
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
// 3、completeUnitOfWork 是回溯节点
completeUnitOfWork(unitOfWork);
} else {
// workInProgress 是一个全局变量,如果 workInProgress 不为 null,
// 在 workLoopSync 和 workLoopConcurrent 中循环执行任务,直到所有的任务执行完
workInProgress = next;
}
}
function completeUnitOfWork(unitOfWork: Fiber): void {
// 尝试完成当前的工作单元,然后移动到下一个兄弟。 如果没有更多的兄弟姐妹,则返回到父节点。
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
let next;
// 3.1 回溯并处理节点
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
// 3.2 判断是否有旁支
if (next !== null) {
// 判断在处理节点的过程中, 是否派生出新的节点
workInProgress = next;
return;
}
}
// 当前的fiber已完成渲染任务,移动到它的兄弟节点,继续执行兄弟节点上的任务
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// 3.3 没有旁支,继续回溯
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
}
深度优先遍历的主要思路是从一个未访问的顶点开始,沿着一条路一直走到底,然后从这条路尽头回退到上一个节点,再从另一条路开始走到底...,不断重复此过程,直到所有的顶点都遍历完成。
在fiber树的构造过程中,从当前工作中的fiber节点 workInProgress 开始,这一步体现在workLoopSync() 函数中。在该函数中调用 performUnitOfWork函数,传入 workInProgress,开始进行深度优先遍历。
沿着一条路一直走到底,体现在performUnitOfWork函数中。在performUnitOfWork函数中调用了beginWork函数向下探寻下一个节点。在beginWork函数中,会根据不同的组件类型,执行不同组件的更新函数。以 updateFunctionComponent 为例,它对外返回的是workInProgress的子节点。
// packages/react-reconciler/src/ReactFiberBeginWork.new.js
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
// ...
return workInProgress.child;
}
由于子节点不为空,因此在workLoopSync函数中继续执行performUnitOfWork函数,向下遍历下一个节点。直到子节点为空后,在performUnitOfWork 函数中执行completeUnitOfWork回溯节点,再从另一条路开始向下遍历,不断重复此过程,直到遍历完所有的节点。
查找context的消费节点
function propagateContextChanges<T>(
workInProgress: Fiber,
contexts: Array<any>,
renderLanes: Lanes,
forcePropagateEntireTree: boolean,
): void {
let fiber = workInProgress.child;
if (fiber !== null) {
// Set the return pointer of the child to the work-in-progress fiber.
fiber.return = workInProgress;
}
while (fiber !== null) {
let nextFiber;
// Visit this fiber.
const list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
} else {
// Traverse down.
nextFiber = fiber.child;
}
fiber = nextFiber;
}
}
当context发生改变之后,也是通过深度优先遍历的方式找出依赖该context的所有子节点。如上述源码,通过while循环向下遍历子节点。
commit阶段DOM节点的删除
在commit阶段,对fiber树进行遍历时,如果fiber上有Deletion副作用标记,则会执行DOM节点的删除操作。在遍历fiber树的过程中,也使用到深度优先遍历。源码如下:
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitNestedUnmounts(
finishedRoot: FiberRoot,
root: Fiber,
nearestMountedAncestor: Fiber,
): void {
// While we're inside a removed host node we don't want to call
// removeChild on the inner nodes because they're removed by the top
// call anyway. We also want to call componentWillUnmount on all
// composites before this host node is removed from the tree. Therefore
// we do an inner loop while we're still inside the host node.
let node: Fiber = root;
while (true) {
// 调用 commitUnmount 卸载 ref ,执行生命周期函数
commitUnmount(finishedRoot, node, nearestMountedAncestor);
// Visit children because they may contain more composite or host nodes.
// Skip portals because commitUnmount() currently visits them recursively.
if (
node.child !== null &&
// If we use mutation we drill down into portals using commitUnmount above.
// If we don't use mutation we drill down into portals here instead.
(!supportsMutation || node.tag !== HostPortal)
) {
// 深度优先遍历向下遍历子树
node.child.return = node;
node = node.child;
continue;
}
// node 与 root 相等时说明整棵树的深度优先遍历已完成
if (node === root) {
return;
}
// 当前子节点没有兄弟节点,说明当前子树已经遍历完成,返回父节点继续深度遍历
while (node.sibling === null) {
if (node.return === null || node.return === root) {
return;
}
node = node.return;
}
// 遍历兄弟节点
node.sibling.return = node.return;
node = node.sibling;
}
}
总结
虚拟DOM是一棵树形结构,即fiber树,对于树的遍历,通常有两种方法:深度优先遍历和广度优先遍历。如果react使用广度优先遍历来遍历fiber树,则可能会导致组件的生命周期时序错乱,而深度优先遍历就可以解决这个问题,因此react diff 算法采用了深度优先遍历。