react diff算法

233 阅读4分钟

diff算法,也是调和过程,就是[[react]]用新的虚拟dom去更新现有的fiber节点的过程。

// 入口函数,主要任务是根据新的虚拟dom的个数来调用不同diff算法
// 单个: reconcileSingleElement
// 多个:reconcileChildrenArray
// 文本或数字:reconcileSingleTextNode
function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber: null,
  newChild: any
) {
  if (typeof newChild === 'object' && newChild !== null){
    // 新的子节点是【单个】虚拟dom
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild
          )
        );
    }
    // 新的子节点是【数组】
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild
      )
    }
  }
  // 新子元素是文本或数字
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    return placeSingleChild(
      reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        '' + newChild
      )
    )
  }
  
  // 说明旧的fiber节点有多余,删除
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

diff单个元素

// 单个虚拟dom
// 尝试复用key和elementType都一样的旧fiber节点
// 复用不了就直接创建新的
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstFiber: Fiber | null,
  element: ReactElement
) {
  const key = element.key;
  let child = currentFirstChild;
  while(child !== null) {
   if (child.key === key) {
     // fiber.elementType保存是组件的类型
     // 函数组件就是函数,类组件就是类定义,浏览器标签就是标签名
     if (element.type === child.elementType) {
       // 组件类型一致,则复用旧的fiber节点
       deleteReainingChildren(returnFiber, child.sibling);
       const existing = useFiber(child, element.props);
       existing.return = returnFiber;
       return existing;
     }
     // 类型不同,则直接删除当前以及后面的所有兄弟节点
     deleteRemainingChildren(returnFiber, child);
     break;
   } else {
     deleteChild(returnFiber, child);
   }
   child = child.sibling;
  }
  
  // 走到这里说明没能复用,则新建一个fiber节点
  const created = createFiberFromElement(element, returnFiber.mode);
  created.return = returnFiber;
  return created;
}

小结

  • 尝试复用相同key的旧fiber元素
  • 如果key一致,但是组件类型不一致,也不会继续查找兄弟节点

diff一个数组

// diff一个数组
// 因为fiber节点是单向链表,所以不能使用【两端匹配】
// 只能的从前往后进行复用尽量多的一样的key,一旦遇到key不匹配则直接使用map匹配
function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: Array<*>
): Fiber | null {
  // 第一次循环
  // 是一个老的fiber节点和一个新的虚拟dom 严格从前往后一对一复用相同key
  // 结束条件:老的fiber节点没有兄弟节点了,新的虚拟dom到最后一个了,一对一复用时key不一致
  let oldFiber = currentFirstChild;
  let newIdx = 0;
  let lastPlacedIndex = 0;
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // 当key一致时,用newChildren[newIdx]去更新oldFiber,得到newFiber
    // 否则updateSlot返回null
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx]
    )
    if (newFiber === null) {
      // 只能使用map匹配了,见第四次循环。
      break;
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      // 保存第一个fiber节点,因为需要返回
      resultingFirstChild = newFiber;
    } else {
      // 通过sibling串联起来
      previousNewFiber.sibling = newFiber;
    }
    
    previousNewFiber = newFiber;
    oldFiber = oldFiber.sibling;
  }
  // ...暂时忽略后面的代码
}
function placeChild(
  newFiber: Fiber,
  lastPlacedIndex: number,
  newIndex: number
) {
  // newIndex就是newFiber的正确位置
  newFiber.index = newIndex;
  const current = newFiber.alternate;
  if (current !== null) {
    // 说明是更新fiber节点的位置
    // oldIndex在屏幕上渲染的fiber节点的位置
    const oldIndex = current.index;
    if (oldIndex < lastPlacedIndex) {
      // oldIndex需要移动位置
      return lastPlacedIndex;
    } else {
      // oldIndex === lastPlacedIndex,说明老fiber节点位置没有发生变化
      // oldIndex > lastPlacedIndex,说明老fiber节点向前移动
      return oldIndex;
    }
  } else {
    // 说明newFiber是新增
    return lastPlacedIndex;
  }
}

移动举例:
old: a -> b -> c -> d
new: [a, d, b, c]
a: 因为oldIndex === lastPlacedIndex, 所以返回oldIndex 0
d: 因为oldIndex > lastPlacedIndex,oldPlacedIndex更新成oldIndex 3
b: 因为oldIndex < lastPlacedIndex,oldPlacedIndex不变,但b需要移动
c: 因为oldIndex > lastPlacedIndex,oldPlacedIndex不变,但c需要移动
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstFiber: Fiber | null,
  element: ReactElement
) {
  // 第一次循环
  // ...代码省略

  // 第二次循环
  // 判断第一次循环结束是不是因为新的虚拟dom到最后了
  // 目的:删除旧的fiber节点即可
  if (newIdx === newChildren.length) {
    // 说明第一次循环,那么只需要删除多余的旧的节点
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }
  
  // 第三次循环
  // 判断第一次循环结束的原因老的fiber节点到最后
  // 目的:为多余的新的虚拟dom创建对应的fiber节点
  if (oldFiber === null) {
    // 说明旧的节点已经结束,那么就为多余的新节点创建对应的fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx]);
      if (previousNewFiber === null) {
        // 保存第一个fiber节点,因为需要返回
        resultingFirstChild = newFiber;
      } else {
        // 通过sibling串联起来
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }
  
  // 第四次循环
  // 就是第一次循环中途遇到key不一样的情况
  // 将余下的fiber的key或者index当作map,用于复用
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFormMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx]
    );
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        if (newFiber.alternate !== null) {
          // 将旧fiber从existingChildren中删除,避免最后被删除。
          existingChildren.delete(
            newFiber.key === null ? newIdx : newFiber.key
          )
        }
      }
      lastPlacedFirstChild = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
  }
  if (shouldTrackSideEffects) {
    // 余下的fiber都是需要删除的
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }
  return resultingFirstChild;
}

小结

  • 会严格尝试复用旧fiber链的前n个元素,直到第n+1个fiber对象和第n+1个虚拟dom key不一致,则不再继续复用。
  • 如果全部的虚拟dom都被复用了,只需要删除多余的旧fiber对象
  • 如果全部的fiber对象都被复用了,只需要为多余的虚拟dom创建对应的fiber对象
  • 那么只能通过key来复用老的fiber对象了

shouldTrackSideEffects

之前的代码段中多处使用了shouldTrackSideEffects,大多数都是作为设置flags之前的判断条件,本质是react区别对待了初次挂载阶段和更新阶段:如果一个fiber节点是初次挂载,那么其后代fiber节点都不需要设置flags,只需要设置初次挂载的fiber节点即可;但是更新阶段,其后代fiber节点需要在diff过程中才知道将要更新、删除或添加,所以在diff过程中设置flags。

// 这里是工厂模式,入参就是shouldTrackSideEffects
function ChildReconciler(shouldTrackSideEffects) {
  function deleteChild() {...}
  
  function placeChild() {...}
  
  function placeSingleChild() {...}
  
  function createChild() {...}
  
  function updateFromMap() {...}
  
  function reconcileChildrenArray() {...}
  
  function reconcileSingleTextNode() {...}
  
  function reconcileSingleElement() {...}
  
  function reconcileChildFibers() {...}
  
  // 就是第一个函数,真正实现功能的函数。
  return reconcileChildFibers;
}
// 使用工厂函数创建2个真正实现功能的函数
const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);

function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren)
  } elese {
    workInProgress.child = reconcileChildFibers(workInProgress, current, nextChildren)
  }
}

小结

  • 挂载节点和更新节点的调和算法的代码是一套,仅仅用shouldTrackSideEffects来是否设置flags