通过源码解读React Diff

react diff 为了减低传统 diff 算法的复杂度,对diff会预设3个限制:

  • 只对同级元素进行 diff:如果一个dom节点在前后两次更新中跨越了层级,那么react不会尝试服用他
  • 两个不同类型的元素会产生不同的树:如果元素由div变成了p,react会销毁div及其子孙节点,并新建p及其子孙节点
  • 通过key来暗示哪些子元素在不同的渲染下能保持稳定

可以分为 单一节点diff 和多节点diff,本质上是比较 currentFiber 和 jsx 对象,以及最终生成 workInProgressFiber

单一节点

js 复制代码
// Handle object types
if (typeof newChild === 'object' && newChild !== null) {
  switch (newChild.$$typeof) {
    case REACT_ELEMENT_TYPE:
      return placeSingleChild(
        reconcileSingleElement(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
          mergeDebugInfo(debugInfo, newChild._debugInfo),
        ),
      );
   // ...
  }
  
  // ...

  throwOnInvalidObjectType(returnFiber, newChild);
}
js 复制代码
function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) {
      // TODO: If key === null and child.key === null, then this only applies to
      // the first item in the list.
      if (child.key === key) {
        const elementType = element.type;
        if (elementType === REACT_FRAGMENT_TYPE) {
          if (child.tag === Fragment) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props.children);
            existing.return = returnFiber;
            
            return existing;
          }
        } else {
          if (
            child.elementType === elementType ||
            (typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props);
            coerceRef(returnFiber, child, existing, element);
            existing.return = returnFiber;
            
            return existing;
          }
        }
        // Didn't match.
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      
      return created;
    } else {
       // 当 key 或者类型不相等时,会根据新创建的 React element 元素创建新的 Fiber 节点
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      // 添加 ref 属性 { current: DOM }
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      // 添加父级 Fiber 对象
      created.return = returnFiber;

      // 返回创建好的子Fiber
      return created;
    }
  }

第一次mount时,会直接生成一个fiber节点并返回

js 复制代码
function reconcileSingleElement(
    returnFiber: Fiber, // 父级fiber
    currentFirstChild: Fiber | null, // currentfiber,由于是mount时,它为null
    element: ReactElement, // jsx 对象
    lanes: Lanes, // 优先级
    debugInfo: ReactDebugInfo | null,
  ): Fiber {
    // ...
  
    if (element.type === REACT_FRAGMENT_TYPE) {
      // ...   
    } else {
      // 当 key 或者类型不相等时,会根据新创建的 React element 元素创建新的 Fiber 节点
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      // 添加 ref 属性 { current: DOM }
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      // 添加父级 Fiber 对象
      created.return = returnFiber;

      // 返回创建好的子Fiber
      return created;
    }
  }

如果上次更新存在对应的更新节点,我们就需要判断dom节点是否可以被复用,我们先看看可复用的状态下

js 复制代码
function reconcileSingleElement(
    returnFiber: Fiber, // 父级fiber
    currentFirstChild: Fiber | null, // currentfiber,由于是mount时,它为null
    element: ReactElement, // jsx 对象
    lanes: Lanes, // 优先级
    debugInfo: ReactDebugInfo | null,
  ): Fiber {
    const key = element.key; // null
    let child = currentFirstChild; // FiberNode<div>

    while (child !== null) {
      // null === null
      if (child.key === key) {
        const elementType = element.type;
        if (elementType === REACT_FRAGMENT_TYPE) {
          // ...
        } else {
          if (
            child.elementType === elementType ||
            (typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
            // 本次更新是单一节点更新,所以需要他的兄弟节点
            deleteRemainingChildren(returnFiber, child.sibling);

            // 复用老的fiber,并更新 props,设置index = 0,sibling = null
            const existing = useFiber(child, element.props);
            coerceRef(returnFiber, child, existing, element);
            existing.return = returnFiber;

            // 返回复用节点
            return existing;
          }
        }

        // ...
        break;
      } else {
        // ...
      }
      // ...
    }

    // ...
  }

我们现在看看不能复用的逻辑,那么什么情况下不一样呢?

  • type 不同
  • key 不同

我们先看 key 值不一样

php 复制代码
function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
  
    while (child !== null) {
      if (child.key === key) {
        // ...
      } else {
        // 删除当前 fiber 节点
        deleteChild(returnFiber, child);
      }

      // 设置兄弟节点,如果没有兄弟节点则为 null,跳出 while 循环
      child = child.sibling;
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      // ...
    } else {
      
    }
  }

如果jsx和当前节点key值不一样,则会删除当前节点,继续匹配他的兄弟节点,以此类推,如果都不匹配,则会新建一个节点。

我们看看 type 不一样

js 复制代码
function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) {
      if (child.key === key) {
        const elementType = element.type;
        if (elementType === REACT_FRAGMENT_TYPE) {
          // ...
        } else {
          if (
            child.elementType === elementType ||
            (typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
            // ...
          }
        }
        
        // 删除当前节点及兄弟节点
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // ...
      }
      // ...
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      // ...
    } else {
       // 当 key 或者类型不相等时,会根据新创建的 React element 元素创建新的 Fiber 节点
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      // 添加 ref 属性 { current: DOM }
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      // 添加父级 Fiber 对象
      created.return = returnFiber;

      // 返回创建好的子Fiber
      return created;
    }
  }

如果标签不同,那么react则会删除当前节点及兄弟节点,然后新建一个fiber节点

多节点

scss 复制代码
if (isArray(newChild)) {
  return reconcileChildrenArray(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
    mergeDebugInfo(debugInfo, newChild._debugInfo),
  );
}
javascript 复制代码
 function reconcileChildrenArray(
    returnFiber: Fiber, // 父节点
    currentFirstChild: Fiber | null, // currentFiber
    newChildren: Array<any>, // 本次更新的 jsx 对象数组
    lanes: Lanes, // 优先级
    debugInfo: ReactDebugInfo | null,
  ): Fiber | null {
    /**
     * 存储第一个子节点 Fiber 对象, 方法返回的也是第一个子节点 Fiber 对象
     * 因为其他子节点 Fiber 对象都存储在上一个子 Fiber 节点对象的 sibling 属性中
     */
    let resultingFirstChild: Fiber | null = null;
    // 上一次创建的 Fiber 对象
    let previousNewFiber: Fiber | null = null;
    
    // 旧节点的 currentFiber(也就是当前遍历到的fiber)
    let oldFiber = currentFirstChild;
    // 新创建的fiber节点对应dom节点的索引位置,为了区分节点位置变化
    let lastPlacedIndex = 0;
    // 当前遍历  newChildren 的索引
    let newIdx = 0;
    // oldFiber 的下一个 oldFiber
    let nextOldFiber = null;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      // ...
    }

    if (newIdx === newChildren.length) {
      // ...
    }

    if (oldFiber === null) {
      // ...
    }

    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    for (; newIdx < newChildren.length; newIdx++) {
      // ...
    }

    if (shouldTrackSideEffects) {
      // ...
    }

    if (getIsHydrating()) {
      // ...
    }
    return resultingFirstChild;
  }

多节点 diff 分为 3 中情况

  • 节点更新
  • 节点新增或减少
  • 节点位置变化

由于 节点更新出现的概率大于节点新增或减少和节点位置变化,所以react会优先处理节点更新,如果不满足节点更新的情况下,才会去处理其他情况。

我们先熟悉下变量名

ini 复制代码
/**
 * 存储第一个子节点 Fiber 对象, 方法返回的也是第一个子节点 Fiber 对象
 * 因为其他子节点 Fiber 对象都存储在上一个子 Fiber 节点对象的 sibling 属性中
 */
let resultingFirstChild: Fiber | null = null;
// 上一次创建的 Fiber 对象
let previousNewFiber: Fiber | null = null;

// 旧节点的 currentFiber(也就是当前遍历到的fiber)
let oldFiber = currentFirstChild;
// 新创建的fiber节点对应dom节点的索引位置,为了区分节点位置变化
let lastPlacedIndex = 0;
// 当前遍历  newChildren 的索引
let newIdx = 0;
// oldFiber 的下一个 oldFiber
let nextOldFiber = null;

首先,我们先看第一轮的遍历

js 复制代码
let resultingFirstChild: Fiber | null = null;
// 上一次创建的 Fiber 对象
let previousNewFiber: Fiber | null = null;

// 旧节点的 currentFiber(也就是当前遍历到的fiber)
let oldFiber = currentFirstChild;
// 新创建的fiber节点对应dom节点的索引位置,为了区分节点位置变化
let lastPlacedIndex = 0;
// 当前遍历  newChildren 的索引
let newIdx = 0;
// oldFiber 的下一个 oldFiber
let nextOldFiber = null;

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  if (oldFiber.index > newIdx) {
    nextOldFiber = oldFiber;
    oldFiber = null;
  } else {
    nextOldFiber = oldFiber.sibling;
  }

  const newFiber = updateSlot(
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    lanes,
    debugInfo,
  );

  if (newFiber === null) {
    if (oldFiber === null) {
      oldFiber = nextOldFiber;
    }

    break;
  }

  if (shouldTrackSideEffects) {
    if (oldFiber && newFiber.alternate === null) {
      deleteChild(returnFiber, oldFiber);
    }
  }

  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
  if (previousNewFiber === null) {
    // TODO: Move out of the loop. This only happens for the first run.
    resultingFirstChild = newFiber;
  } else {
    previousNewFiber.sibling = newFiber;
  }

  previousNewFiber = newFiber;
  oldFiber = nextOldFiber;
}



// ******
function updateSlot(
  returnFiber: Fiber, // 父节点
  oldFiber: Fiber | null, // currentFiber
  newChild: any, // 需要更新的JSX对象
  lanes: Lanes,
  debugInfo: null | ReactDebugInfo,
): Fiber | null {
  // Update the fiber if the keys match, otherwise return null.
  const key = oldFiber !== null ? oldFiber.key : null;

  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number' ||
    (enableBigIntSupport && typeof newChild === 'bigint')
  ) {
    if (key !== null) {
      return null;
    }
    
    return updateTextNode(
      returnFiber,
      oldFiber,
      '' + newChild,
      lanes,
      debugInfo,
    );
  }

  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE: {
        if (newChild.key === key) {
          return updateElement(
            returnFiber,
            oldFiber,
            newChild,
            lanes,
            mergeDebugInfo(debugInfo, newChild._debugInfo),
          );
        } else {
          return null;
        }
      }
      // ...
    }

    // ...

    throwOnInvalidObjectType(returnFiber, newChild);
  }

  return null;
}

function updateElement(
  returnFiber: Fiber,
  current: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
  debugInfo: ReactDebugInfo | null,
): Fiber {
  const elementType = element.type;
  if (elementType === REACT_FRAGMENT_TYPE) {
    // ...
  }
  
  if (current !== null) {
    if (
      current.elementType === elementType ||
      (typeof elementType === 'object' &&
        elementType !== null &&
        elementType.$$typeof === REACT_LAZY_TYPE &&
        resolveLazy(elementType) === current.type)
    ) {
      // Move based on index
      const existing = useFiber(current, element.props);
      coerceRef(returnFiber, current, existing, element);
      existing.return = returnFiber;
      return existing;
    }
  }
  
  // 如果标签不同,则无法复用currentFiber,需要基于 element 创建一个新的fiber
  const created = createFiberFromElement(element, returnFiber.mode, lanes);
  coerceRef(returnFiber, current, created, element);
  created.return = returnFiber;
  
  return created;
}

看了代码后,我们可以看到,如果 key 和 type 不同,则会跳出第一个循环。另外,如果 key 相同 type 不同导致不可复用,会将 oldFiber 标记 DELETION 的标记,并继续遍历

js 复制代码
const newFiber = updateSlot(
  returnFiber,
  oldFiber,
  newChildren[newIdx],
  lanes,
  debugInfo,
);

if (newFiber === null) {
  if (oldFiber === null) {
    oldFiber = nextOldFiber;
  }
  break;
}

if (shouldTrackSideEffects) {
  if (oldFiber && newFiber.alternate === null) {
    // 将 oldFiber 加入到 returnFiber 的 deletions 数组中,后续删除
    deleteChild(returnFiber, oldFiber);
  }
}
js 复制代码
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  // ...
}

// 当前遍历 newChildren 的索引 === newChildren的长度(最理想的结果)
if (newIdx === newChildren.length) {
  // 删除其他还没有遍历的oldFiber
  deleteRemainingChildren(returnFiber, oldFiber);
  return resultingFirstChild;
}

// 旧节点的 currentFiber 已经遍历完了,这种情况处于还有其他新增节点没遍历
// newChildren没遍历完,oldFiber遍历完(第二种情况)
if (oldFiber === null) {
  // 需要遍历剩下的newChildren
  for (; newIdx < newChildren.length; newIdx++) {
    // 创建新的 fiber 节点
    const newFiber = createChild(
      returnFiber,
      newChildren[newIdx],
      lanes,
      debugInfo,
    );
    
    if (newFiber === null) {
      continue;
    }
    // 标记为新增
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
  
  return resultingFirstChild;
}

// newChildren 与 oldFiber 都没遍历完(第三种情况)

// oldFiber 生成 key 对应 fiber节点的映射
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

// 处理移动的节点
for (; newIdx < newChildren.length; newIdx++) {
  // ...
}

if (shouldTrackSideEffects) {
  // 删除 existingChildren 未匹配到的 fiber
  existingChildren.forEach(child => deleteChild(returnFiber, child));
} 

当第一个遍历完成后,react 会判断当前是否将 newChildren遍历完成了,如果没有则会再去判断 oldFiber 是否有值,没有代表 newChildren 中都是新增节点。接下来遍历剩余的newChildren,通过newChildren[i].key就能在existingChildren中找到key相同的oldFiber。

js 复制代码
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

for (; newIdx < newChildren.length; newIdx++) {
  const newFiber = updateFromMap(
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx], // 当前的 element
    lanes,
    debugInfo,
  );
  
  if (newFiber !== null) {
    if (shouldTrackSideEffects) {
      // newFiber 是复用节点
      if (newFiber.alternate !== null) {
        existingChildren.delete(
          newFiber.key === null ? newIdx : newFiber.key,
        );
      }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
}

//*****************************

function updateFromMap(
    existingChildren: Map<string | number, Fiber>,
    returnFiber: Fiber,
    newIdx: number,
    newChild: any,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber | null {
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number' ||
      (enableBigIntSupport && typeof newChild === 'bigint')
    ) {
      // 获取对应的 oldFiber
      const matchedFiber = existingChildren.get(newIdx) || null;
      return updateTextNode(
        returnFiber,
        matchedFiber,
        '' + newChild,
        lanes,
        debugInfo,
      );
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          const matchedFiber =
            existingChildren.get(
              newChild.key === null ? newIdx : newChild.key,
            ) || null; // 获取对应的 oldFiber
          
          return updateElement(
            returnFiber,
            matchedFiber,
            newChild,
            lanes,
            mergeDebugInfo(debugInfo, newChild._debugInfo),
          );
        }
        // ...
      }
      
      // ...

      throwOnInvalidObjectType(returnFiber, newChild);
    }

    return null;
  }
相关推荐
micro2010143 分钟前
Microsoft Edge 离线安装包制作或获取方法和下载地址分享
前端·edge
.生产的驴7 分钟前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
awonw10 分钟前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
九圣残炎31 分钟前
【Vue】vue-admin-template项目搭建
前端·vue.js·arcgis
柏箱1 小时前
使用JavaScript写一个网页端的四则运算器
前端·javascript·css
TU^1 小时前
C语言习题~day16
c语言·前端·算法
学习使我快乐014 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19954 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈5 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts