关于前端:useEffect-返回的函数是怎么执行的

4次阅读

共计 2855 个字符,预计需要花费 8 分钟才能阅读完成。

本文对应的 react 版本是 18.2.0

在正式开始之前,咱们要理解一个 fiber 的属性:deletions

这个属性寄存的是以后节点中被删除的 fiber,这个数组是在 commit 阶段被赋值的

如果有被删除的节点,这个属性值是一个数组,如果没有被删除的节点,这个属性值是 null

const A = () => {useEffect(() => {return () => {console.log("A unmount");
    };
  }, []);
  return <div> 文本 A </div>;
};
const B = () => {useEffect(() => {return () => {console.log("B unmount");
    };
  }, []);
  return <div> 文本 B </div>;
};

如果 App 组件这样写,那么 deletions 的值是 [FiberNode, FiberNode]

const App(){const [count, setCount] = useState(0)

  return <div>
    {count % 2 === 0 && <A />}
    {count % 2 === 0 && <B />}
    <div onClick={()=> setCount(count+1)}>+1</div>
  </div>
}

如果 App 组件这样写,那么 deletions 的值是 [FiberNode]

const App(){const [count, setCount] = useState(0)

  return <div>
    {count % 2 === 0 && <><A /><B /></>}
    <div onClick={()=> setCount(count+1)}>+1</div>
  </div>
}

对于第二种状况,react 会把 A 组件和 B 组件作为一个整体,所以 deletions 的值是 [FiberNode]

解决以后节点的 deletions

react 在遍历 fiber tree 时,会先解决以后的 fiberdeletions,等解决完之后再遍历下一个 fiber

当初咱们曾经晓得 deletions 中保留的是以后 fiber 下被删除的子节点

这时 react 会遍历 deletions 数组,而后执行每个 fiberpassive effect 返回的函数

然而有个问题,如果 deletions 中的 fiber 有子节点,那么这些子节点也会被删除,这时 react 会怎么解决呢?

这里分两种状况来探讨:

  1. 删除的 fiber 没有子节点:<div>{xxxx && <A />}</div>
  2. 删除的 fiber 有子节点:<div>{xxxx && <><A /><B /></>}</div> –>

删除的 fiber 没有子节点:<div>{xxxx && <A />}</div>

这种状况比拟好了解

当遍历到 div 时,因为 <A/> 节点会被卸载,所以在 divdeletions 保留了一个 <A/>fiber

遍历 deletions 数组,执行 <A/>passive effect 返回的函数

如下图所示:

删除的 fiber 有子节点:<div>{xxxx && <><A /><B /></>}</div>

这种状况就比较复杂了

当遍历到 div 时,<></> 节点会被卸载,所以在 divdeletions 保留了一个 <></>fiber

遍历 deletions 数组,执行 fiberpassive effect 返回的函数,对于 <></> 来说是不存在的 passive effect

那么这个时候就要去遍历它的 child.fiber,也就是 <A/><B/>

首先拿到第一个 fiber,也就是 <A/>,而后执行 <A/>passive effect 返回的函数,这步比拟好了解

child = fiber.child;
if (child !== null) {nextEffect = child;}

这里遍历也是深度优先,遍历一个 child,执行一个 passive effect 返回函数,而后再遍历下一个 child(这边 <A /> 曾经是叶子节点了 )

而后拿到第二个 fiber,也就是 <B/>,而后执行 <B/>passive effect 返回的函数,这步就不太好了解了

child = fiber.child;
if (child !== null) {nextEffect = child;} else {commitPassiveUnmountEffectsInsideOfDeletedTree_complete(deletedSubtreeRoot);
}

这里要留神的是:

react 在寻找有 passive effectfiber 时,只遍历到有 passive effectfiber,像 div 这种没有 passive effect 就不会遍历

然而在解决 deletionsreact 会遍历所有的 fiber,也就是说从以后的 fiber 开始,始终往下遍历到叶子节点,这个叶子节点是指文本节点这种,往下不会有节点了(对于 A 组件来说 文本 A 是文本节点)

而后在开始往上遍历,往上遍历是调用 commitPassiveUnmountEffectsInsideOfDeletedTree_complete 函数,直到遍历到 deletionRoot,在向上遍历的过程中会查看是否有 sibling,如果有阐明 sibling 还没被解决,这样就找到了 <B/>,而后执行 <B/>passive effect 返回的函数

如下图所示:

向下遍历和向上遍历

在解决 deletions 时,对于每个 deletedNode,都先向下遍历,而后再向上遍历

  • 向下遍历:commitPassiveUnmountEffectsInsideOfDeletedTree_begin(深度优先,优先解决右边的节点)
  • 向上遍历:commitPassiveUnmountEffectsInsideOfDeletedTree_complete(之后再解决左边节点)

总结

1. 遍历 deletions 数组:

  • react 在解决 deletions 时,先沿着 fiber tree 向下遍历,如果有 passive effect 返回的函数,则执行
  • 始终遍历到没有 childfiber,再向上遍历,解决 sibling
  • 再向上遍历时,如果如果遇到 sibling,再向下遍历,向下遍历时遇到 passive effect 返回的函数,则执行
  • 如此循环直到遍历到 deletedNode,完结遍历

2. 联合把握 React 组件树遍历技巧

  • 遍历寻找有 passive effect 节点

    • react 从根组件向下遍历,如果没有 passive effect,则不会遍历
  • 遍历时,如果遇到以后节点有 deletions 时,会暂停寻找 passive effect 节点

    • 进入遍历 deletions 数组

react 遍历 deletions 残缺逻辑如下图所示:

图中绿色局部是遍历 deletionsNode 过程,红色局部是遍历寻找 passive effect 过程

往期文章

  1. 深刻探索 React 原生事件的工作原理
  2. React Lane 算法:一文详解 8 种 Lane 操作
  3. 分析 React 任务调度机制:scheduleCallback 实现原理
  4. 把握 React 组件树遍历技巧

更多 react 源码文章

正文完
 0