• 【React源码】(十七)React 算法之深度优先遍历


    React 算法之深度优先遍历

    对于树或图结构的搜索(或遍历)来讲, 分为深度优先(DFS)和广度优先(BFS).

    概念

    深度优先遍历: DFS(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法.

    来自 wiki 上的解释(更权威): 当节点v的所在边都己被探寻过, 搜索将回溯到发现节点v的那条边的起始节点. 这一过程一直进行到已发现从源节点可达的所有节点为止. 如果还存在未被发现的节点, 则选择其中一个作为源节点并重复以上过程, 整个进程反复进行直到所有节点都被访问为止.

    实现方式

    DFS 的主流实现方式有 2 种.

    1. 递归(简单粗暴)
    2. 利用存储遍历路径
     
    
    1. function Node() {
    2. this.name = '';
    3. this.children = [];
    4. }
    5. function dfs(node) {
    6. console.log('探寻阶段: ', node.name);
    7. node.children.forEach(child => {
    8. dfs(child);
    9. });
    10. console.log('回溯阶段: ', node.name);
    11. }
    1. 使用栈
     
    
    1. function Node() {
    2. this.name = '';
    3. this.children = [];
    4. // 因为要分辨探寻阶段和回溯阶段, 所以必须要一个属性来记录是否已经访问过该节点
    5. // 如果不打印探寻和回溯, 就不需要此属性
    6. this.visited = false;
    7. }
    8. function dfs(node) {
    9. const stack = [];
    10. stack.push(node);
    11. // 栈顶元素还存在, 就继续循环
    12. while ((node = stack[stack.length - 1])) {
    13. if (node.visited) {
    14. console.log('回溯阶段: ', node.name);
    15. // 回溯完成, 弹出该元素
    16. stack.pop();
    17. } else {
    18. console.log('探寻阶段: ', node.name);
    19. node.visited = true;
    20. // 利用栈的先进后出的特性, 倒序将节点送入栈中
    21. for (let i = node.children.length - 1; i >= 0; i--) {
    22. stack.push(node.children[i]);
    23. }
    24. }
    25. }
    26. }

    React 当中的使用场景

    深度优先遍历在react当中的使用非常典型, 最主要的使用时在ReactElementfiber树的构造过程. 其次是在使用context时, 需要深度优先地查找消费context的节点.

    ReactElement "树"的构造

    ReactElement不能算是严格的树结构, 为了方便表述, 后文都称之为树.

    react-reconciler包中, ReactElement的构造过程实际上是嵌套在fiber树构造循环过程中的, 与fiber树的构造是相互交替进行的(在fiber 树构建章节中详细解读, 本节只介绍深度优先遍历的使用场景).

    ReactElement树的构造, 实际上就是各级组件render之后的总和. 整个过程体现在reconciler工作循环之中.

    源码位于ReactFiberWorkLoop.js中, 此处为了简明, 已经将源码中与 dfs 无关的旁支逻辑去掉.

     
    
    1. function workLoopSync() {
    2. // 1. 最外层循环, 保证每一个节点都能遍历, 不会遗漏
    3. while (workInProgress !== null) {
    4. performUnitOfWork(workInProgress);
    5. }
    6. }
    7. function performUnitOfWork(unitOfWork: Fiber): void {
    8. const current = unitOfWork.alternate;
    9. let next;
    10. // 2. beginWork是向下探寻阶段
    11. next = beginWork(current, unitOfWork, subtreeRenderLanes);
    12. if (next === null) {
    13. // 3. completeUnitOfWork 是回溯阶段
    14. completeUnitOfWork(unitOfWork);
    15. } else {
    16. workInProgress = next;
    17. }
    18. }
    19. function completeUnitOfWork(unitOfWork: Fiber): void {
    20. let completedWork = unitOfWork;
    21. do {
    22. const current = completedWork.alternate;
    23. const returnFiber = completedWork.return;
    24. let next;
    25. // 3.1 回溯并处理节点
    26. next = completeWork(current, completedWork, subtreeRenderLanes);
    27. if (next !== null) {
    28. // 判断在处理节点的过程中, 是否派生出新的节点
    29. workInProgress = next;
    30. return;
    31. }
    32. const siblingFiber = completedWork.sibling;
    33. // 3.2 判断是否有旁支
    34. if (siblingFiber !== null) {
    35. workInProgress = siblingFiber;
    36. return;
    37. }
    38. // 3.3 没有旁支 继续回溯
    39. completedWork = returnFiber;
    40. workInProgress = completedWork;
    41. } while (completedWork !== null);
    42. }

    以上源码本质上是采用递归的方式进行 dfs, 假设有以下组件结构:

     
    
    1. class App extends React.Component {
    2. render() {
    3. return (
    4. <div className="app">
    5. <header>headerheader>
    6. <Content />
    7. <footer>footerfooter>
    8. div>
    9. );
    10. }
    11. }
    12. class Content extends React.Component {
    13. render() {
    14. return (
    15. <React.Fragment>
    16. <p>1p>
    17. <p>2p>
    18. <p>3p>
    19. React.Fragment>
    20. );
    21. }
    22. }
    23. export default App;

    则可以绘制出遍历路径如下:

    注意:

    • ReactElement树是在大循环中的beginWork阶段"逐级"生成的.
    • "逐级"中的每一级是指一个classfunction类型的组件, 每调用一次render或执行一次function调用, 就会生成一批ReactElement节点.
    • ReactElement树的构造, 实际上就是各级组件render之后的总和.

    fiber 树的构造

    ReactElement的构造过程中, 同时伴随着fiber树的构造, fiber树同样也是在beginWork阶段生成的.

    绘制出遍历路径如下:

    查找 context 的消费节点

    context改变之后, 需要找出依赖该context的所有子节点(详细分析会在context原理章节深入解读), 这里同样也是一个DFS, 具体源码在ReactFiberNewContext.js.

    将其主干逻辑剥离出来, 可以清晰的看出采用循环递归的方式进行遍历:

     
    
    1. export function propagateContextChange(
    2. workInProgress: Fiber,
    3. context: ReactContext,
    4. changedBits: number,
    5. renderLanes: Lanes,
    6. ): void {
    7. let fiber = workInProgress.child;
    8. while (fiber !== null) {
    9. let nextFiber;
    10. // Visit this fiber.
    11. const list = fiber.dependencies;
    12. if (list !== null) {
    13. // 匹配context等逻辑, 和dfs无关, 此处可以暂时忽略
    14. // ...
    15. } else {
    16. // 向下探寻
    17. nextFiber = fiber.child;
    18. }
    19. fiber = nextFiber;
    20. }
    21. }

    总结

    由于react内部使用了ReactElementfiber两大树形结构, 所以有不少关于节点访问的逻辑.

    本节主要介绍了DFS的概念和它在react源码中的使用情况. 其中fiber树的DFS遍历, 涉及到的代码多, 分布广, 涵盖了reconciler阶段的大部分工作, 是reconciler阶段工作循环的核心流程.

    除了DFS之外, 源码中还有很多逻辑都是查找树中的节点(如: 向上查找父节点等). 对树形结构的遍历在源码中的比例很高, 了解这些算法技巧能够更好的理解react源码.

    参考资料

    深度优先搜索

  • 相关阅读:
    Windows 10 安装配置WSL2(Ubuntu 20.04)教程
    进程中的权限是如何操作的
    无swing,高级javaSE毕业之贪吃蛇游戏(含模块构建,多线程监听服务),已录制视频
    人工智能轨道交通行业周刊-第21期(2022.10.31-11.6)
    【读书笔记】《学习究竟是什么》——学精第一
    leetcode解题思路分析(一百四十九)1297 - 1304 题
    Nginx中间件服务:负载均衡(调度算法)
    服装企业为什么要谈信息化?
    OSPF高级特性 —— 被动接口 + 按需链路 + donotage标记
    Vim 插件应用篇 vim-plug:简洁高效的Vim插件管理工具
  • 原文地址:https://blog.csdn.net/weixin_44828588/article/details/126546370