本章为选读章节

是否学习该章对后续章节的学习没有影响。

beginWork一节我们提到

对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点。

这一章我们讲解Diff算法的实现。

你可以从这里概览 - 图1 (opens new window)看到Diff算法的介绍。

为了防止概念混淆,这里再强调下

一个DOM节点在某一时刻最多会有4个节点和他相关。

  1. current Fiber。如果该DOM节点已在页面中,current Fiber代表该DOM节点对应的Fiber节点

  2. workInProgress Fiber。如果该DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该DOM节点对应的Fiber节点

  3. DOM节点本身。

  4. JSX对象。即ClassComponentrender方法的返回结果,或FunctionComponent的调用结果。JSX对象中包含描述DOM节点的信息。

Diff算法的本质是对比1和4,生成2。

Diff的瓶颈以及React如何应对

由于Diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中n是树中元素的数量。

如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。

为了降低算法复杂度,Reactdiff会预设三个限制:

  1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。

  2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。

  3. 开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:

  1. // 更新前
  2. <div>
  3. <p key="ka">ka</p>
  4. <h3 key="song">song</h3>
  5. </div>
  6. // 更新后
  7. <div>
  8. <h3 key="song">song</h3>
  9. <p key="ka">ka</p>
  10. </div>

如果没有keyReact会认为div的第一个子节点由p变为h3,第二个子节点由h3变为p。这符合限制2的设定,会销毁并新建。

但是当我们用key指明了节点前后对应关系后,React知道key === "ka"p在更新后还存在,所以DOM节点可以复用,只是需要交换下顺序。

这就是React为了应对算法性能瓶颈做出的三条限制。

Diff是如何实现的

我们从Diff的入口函数reconcileChildFibers出发,该函数会根据newChild(即JSX对象)类型调用不同的处理函数。

你可以从这里概览 - 图2 (opens new window)看到reconcileChildFibers的源码。

  1. // 根据newChild类型选择不同diff函数处理
  2. function reconcileChildFibers(
  3. returnFiber: Fiber,
  4. currentFirstChild: Fiber | null,
  5. newChild: any,
  6. ): Fiber | null {
  7. const isObject = typeof newChild === 'object' && newChild !== null;
  8. if (isObject) {
  9. // object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
  10. switch (newChild.$$typeof) {
  11. case REACT_ELEMENT_TYPE:
  12. // 调用 reconcileSingleElement 处理
  13. // // ...省略其他case
  14. }
  15. }
  16. if (typeof newChild === 'string' || typeof newChild === 'number') {
  17. // 调用 reconcileSingleTextNode 处理
  18. // ...省略
  19. }
  20. if (isArray(newChild)) {
  21. // 调用 reconcileChildrenArray 处理
  22. // ...省略
  23. }
  24. // 一些其他情况调用处理函数
  25. // ...省略
  26. // 以上都没有命中,删除节点
  27. return deleteRemainingChildren(returnFiber, currentFirstChild);
  28. }

我们可以从同级的节点数量将Diff分为两类:

  1. newChild类型为objectnumberstring,代表同级只有一个节点

  2. newChild类型为Array,同级有多个节点

在接下来两节我们会分别讨论这两类节点的Diff