对于单个节点,我们以类型object为例,会进入reconcileSingleElement

你可以从这里看到reconcileSingleElement源码

  const isObject = typeof newChild === 'object' && newChild !== null;

  if (isObject) {
    // 对象类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        // 调用 reconcileSingleElement 处理
      // ...其他case
    }
  }

这个函数会做如下事情:

diff

让我们看看第二步判断DOM节点是否可以复用是如何实现的。

function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  
  // 首先判断是否存在对应DOM节点
  while (child !== null) {
    // 上一次更新存在DOM节点,接下来判断是否可复用

    // 首先比较key是否相同
    if (child.key === key) {

      // key相同,接下来比较type是否相同

      switch (child.tag) {
        // ...省略case
        
        default: {
          if (child.elementType === element.type) {
            // type相同则表示可以复用
            // 返回复用的fiber
            return existing;
          }
          
          // type不同则跳出switch
          break;
        }
      }
      // 代码执行到这里代表:key相同但是type不同
      // 将该fiber及其兄弟fiber标记为删除
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // key不同,将该fiber标记为删除
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }

  // 创建新Fiber,并返回 ...省略
}

还记得我们刚才提到的,React预设的限制么,

从代码可以看出,React通过先判断key是否相同,如果key相同则判断type是否相同,只有都相同时一个DOM节点才能复用。

这里有个细节需要关注下:

  • child !== nullkey相同type不同时执行deleteRemainingChildrenchild及其兄弟fiber都标记删除。

  • child !== nullkey不同时仅将child标记删除。

考虑如下例子:

当前页面有3个li,我们要全部删除,再插入一个p

// 当前页面显示的
ul > li * 3

// 这次需要更新的
ul > p

由于本次更新时只有一个p,属于单一节点的Diff,会走上面介绍的代码逻辑。

reconcileSingleElement中遍历之前的3个fiber(对应的DOM为3个li),寻找本次更新的p是否可以复用之前的3个fiber中某个的DOM

key相同type不同时,代表我们已经找到本次更新的p对应的上次的fiber,但是pli type不同,不能复用。既然唯一的可能性已经不能复用,则剩下的fiber都没有机会了,所以都需要标记删除。

key不同时只代表遍历到的该fiber不能被p复用,后面还有兄弟fiber还没有遍历到。所以仅仅标记该fiber删除。

# 练习题

让我们来做几道习题巩固下吧:

请判断如下JSX对象对应的DOM元素是否可以复用:

// 习题1 更新前
<div>ka song</div>
// 更新后
<p>ka song</p>

// 习题2 更新前
<div key="xxx">ka song</div>
// 更新后
<div key="ooo">ka song</div>

// 习题3 更新前
<div key="xxx">ka song</div>
// 更新后
<p key="ooo">ka song</p>

// 习题4 更新前
<div key="xxx">ka song</div>
// 更新后
<div key="xxx">xiao bei</div>

公布答案:

习题1: 未设置key prop默认 key = null;,所以更新前后key相同,都为null,但是更新前typediv,更新后为ptype改变则不能复用。

习题2: 更新前后key改变,不需要再判断type,不能复用。

习题3: 更新前后key改变,不需要再判断type,不能复用。

习题4: 更新前后keytype都未改变,可以复用。children变化,DOM的子元素需要更新。

你是不是都答对了呢。