likes
comments
collection
share

React Diff原理

作者站长头像
站长
· 阅读数 62

目录

前言

本文是React源码学习系列第五篇,该系列整体都基于React18.0.0版本源码。旨在学习过程中记录一些个人的理解。该篇介绍React Diff原理。

概念

在beginWork中没有命中优化策略的FiberNode会根据所处阶段的不同(mount或update)进入mountChildFibers或者reconcileChildFibers,它们的区别是是否追踪副作用(是否标记flags)。这一流程统称为reconcile流程。reconcile的本质是对比current Fiber Tree和JSX对象(VDOM),生成workInProgress Fiber Tree的过程。这一过程的核心算法就是Diff算法。

Diff算法本身会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全的对比复杂度为O(n3),其中n为树中元素的数量。为了降低算法复杂度,React的Diff算法会预设三个限制。

  1. 只对同级元素进行Diff。如果一个DOM元素在前后两次更新中跨越了层级,React不会尝试复用它。
  2. 两个不同类型的元素会产生不同的树,元素类型变更,React会销毁变更前元素及其子孙元素,并新建更新后元素及其子孙元素。
  3. 开发中可以通过key属性来帮助Diff。

例子

<!--更新前-->
<div>
    <p key="jay">jay</p>
    <h3 key="guo">guo</h3>
</div>
<!--更新后-->
<div>
    <h3 key="guo">guo</h3>
    <p key="jay">jay</p>
</div>

如果没有key,React会认为div的第一个子元素由p变更为h3,第二个子元素由h3变更为p。这符合限制2。当有了key来帮助前后对应关系后,React会知道元素可以复用,只需要调换顺序。

入口函数

// 保留核心代码
const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  // 初次渲染除了HostRootFiber其它都走这里
  if (current === null) { // 不追踪副作用
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else { // 追踪副作用
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

reconcileChildFibers

// 保留核心代码
function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    // 单个非文本节点
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return // 省略处理逻辑
      }
      // array类型 
      if (isArray(newChild)) {
        return // 省略处理逻辑
      }
      // iterator
      if (getIteratorFn(newChild)) {
        return // 省略处理逻辑
      }
    }
     // 文本节点
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      return // 省略处理逻辑
    }
    // 没有匹配到的打上删除标记
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

根据限制1,“只对同级别元素进行Diff”,可以将Diff流程分为两类。

  1. 更新后只有一个元素。
  2. 更新后有多个元素。

单节点Diff

if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
        // 单个非文本节点
        case REACT_ELEMENT_TYPE:
            return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes)
        );
    }
}

placeSingleChild

placeSingleChild方法,根据是否最终副作用,决定是否给新Fiber打上Placement标记。

  1. HostRootFiber因为初始化的时候就有alternate,所以无论是mount还是update它的child(newFiber)都会打上Placement标记.
  2. 从而实现初次渲染的时候只插入一次DOM。
function placeSingleChild(newFiber: Fiber): Fiber {
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags |= Placement;
    }
    return newFiber;
  }

reconcileSingleElement

// 保留关键代码
function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    // 遍历寻找可复用的老Fiber节点
    while (child !== null) {
      // key是否相同
      if (child.key === key) {
        const elementType = element.type;
        if (elementType === REACT_FRAGMENT_TYPE) {
          // 忽略FRAGMENT
        } else {
          // 找到key、type相同的老Fiber节点
          if (child.elementType === elementType) {
            // 给老节点的其他兄弟节点打上删除标记。
            deleteRemainingChildren(returnFiber, child.sibling);
            // 根据老Fiber clone一个新Fiber
            // useFiber最终调用createWorkInProgress,clone一个Fiber
            const existing = useFiber(child, element.props);
            existing.ref = coerceRef(returnFiber, child, element);
            existing.return = returnFiber
            return existing;
          }
        }
        // key相同、但是类型不同,说明老节点都不可以复用,给所有老节点都打上删除标记,并退出循环,走后面新建fiber逻辑
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // key不相同 打上删除标记
        deleteChild(returnFiber, child);
      }
      // 继续下一个节点,判断是否可复用
      child = child.sibling;
    }
    // 老节点没有找到可复用的,会走到这里,新建一个fiber并返回
    if (element.type === REACT_FRAGMENT_TYPE) {
      // 忽略REACT_FRAGMENT_TYPE
    } else {
      // 实际调用createFiber创建一个新的Fiber
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

reconcileSingleTextNode

新的children为单个文本节点走reconcileSingleTextNode方法。该方法内部判断是否可以复用老节点。

// 保留关键代码
function reconcileSingleTextNode(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    textContent: string,
    lanes: Lanes,
  ): Fiber {
    // 老节点是文本
    if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
      // 给所有兄弟Fiber打上删除标记
      deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
      // 复用老节点
      const existing = useFiber(currentFirstChild, textContent);
      existing.return = returnFiber;
      return existing;
    }
    // 老节点不是文本,不能复用,给所有老节点都打上删除标记
    deleteRemainingChildren(returnFiber, currentFirstChild);
    // 新建一个新的fiber
    const created = createFiberFromText(textContent, returnFiber.mode, lanes);
    created.return = returnFiber;
    return created;
}
  1. 首先判断key是否相同
  2. key相同在判断type是否相同。
  3. key相同,type不同说明没有可复用节点,全部打上删除标记。
  4. 都不相同,继续匹配,直到所有元素都匹配完。

多节点Diff

多节点Diff一定属于以下一种或者多种。

  1. 节点位置没有变化。
  2. 节点增删。
  3. 节点移动。

设计思路

根据以上三种情况,一种常见的Diff算法设计思路是:

  1. 判断当前节点是属于哪种情况。
  2. 如果是增删,执行增删操作。
  3. 如果位置没有变化,执行相应逻辑。
  4. 如果有移动,执行移动逻辑。

这个方案的前提是所有操作的优先级相同,但通常在日常开发中,“节点移动”较少发生,所以Diff算法会优先判断其他情况。基于这个理念,Diff算法的整体逻辑会经历两次遍历。

  1. 第一次遍历遍历尝试逐个复用节点。
  2. 第二轮遍历处理剩下的节点。

第一次遍历

// 参与比较的current Fiber
let oldFiber = currentFirstChild;
// 最后一个可复用oldFiber的位置索引
let lastPlacedIndex = 0;
// jsx对象索引
let newIdx = 0;
// 下一个oldFiber
let nextOldFiber = null;
// 老节点为链表,新节点为虚拟DOM数组
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // ...
}
  1. 遍历newChildren,将newChildren[newIdx]和oldFiber比较判断是否可复用。
  2. 如果可复用,继续步骤1,如果不可复用:2.1:key不相同,直接跳出循环,第一次遍历结束;2.2:key相同,type不相同,给oldFiber打上Deletion标记,继续步骤1;
  3. 如果newChlidren或者oldFiber遍历完,跳出循环,第一次遍历结束。

第二次遍历

进入第二次遍历有3种情况。

1.oldFiber遍历完了,newChildren未遍历完。 2.newChildren遍历完了,oldFiber未遍历完。 3.newChlidren和oldFiber都未遍历完。 情况3:

情况1

给未遍历完的newChildren节点打上Placement标记。

if (oldFiber === null) {
    // 剩余的新节点都执行新建操作
    for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
            continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
            resultingFirstChild = newFiber;
        } else {
            previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
    }
    return resultingFirstChild;
}

情况2

给未遍历完的oldFiber节点都打上Deletion标记。

if (newIdx === newChildren.length) {
    // 把剩余的老节点打上删除标记
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
}

情况3

产生这种情况的原因是节点发生了移动。导致不能按照顺序遍历完新老节点。将未处理的oldFiber生成一个Map, key为Fiber.key或者Fiber.index,value为Fiber。

mapRemainingChildren

// 保留核心代码
function mapRemainingChildren(
    returnFiber: Fiber,
    currentFirstChild: Fiber,
  ): Map<string | number, Fiber> {
    const existingChildren: Map<string | number, Fiber> = new Map();
    
    let existingChild = currentFirstChild;
    while (existingChild !== null) {
      if (existingChild.key !== null) {
        existingChildren.set(existingChild.key, existingChild);
      } else {
        existingChildren.set(existingChild.index, existingChild);
      }
      existingChild = existingChild.sibling;
    }
    return existingChildren;
}

遍历未处理的newChildren。

for (; newIdx < newChildren.length; newIdx++) {
    const matchedFiber = existingChildren.get(newChild.key === null ? newIdx : newChild.key) || null;
}

当找到可以复用的oldFiber后,如何确定位置有没有变化?判断的参考物是lastPlacedIndex变量,即“最后一个可复用oldFiber的位置索引”。 因为newChildren中jsx对象的顺序代表“本次更新后对应FiberNode的顺序”,所以在遍历newChildren过程中,每个新生成的wip FiberNode一定是当前最靠右的节点。 可以分两种情况:

  1. oldFiber.index < lastPlacedIndex
  2. oldFiber.index >= lastPlacedIndex
情况1

说明oldFiber在lastPlacedIndex左边。已知wip FiberNode是目前最右边的一个元素,不会在lastPlacedIndex的左边,所以oldFiber发生了移动,需要打上Placement标记。

情况2

说明oldFiber不在lastPlacedIndex左边。与wip FiberNode位置一致,不需要移动。

placeChild

判断“是否移动”逻辑在placeChild方法。

// 插入或者移动Fiber 这个方法很重要
function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number, // 最后一个可复用老节点的索引。
    newIndex: number, // 新children数组索引为止,因为数组是从左往右遍历的,当前新节点位置肯定是最右边的位置
  ): number {
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    // 说明是复用的老节点
    if (current !== null) {
      const oldIndex = current.index;
      // 可复用老节点索引小于lastPlacedIndex,而新节点肯定是当前最右边一个,说明该老节点需要移动,打上标记。
      if (oldIndex < lastPlacedIndex) {
        // 这个节点需要移动 打上Placement标记
        newFiber.flags |= Placement;
        return lastPlacedIndex;
      } else { // 否则不需要移动,并把oldIndex赋值给lastPlacedIndex
        // 这个节点不需要移动。
        return oldIndex;
      }
    } else {
      // 没有复用 打上Placement标记
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    }
}

例子

// 更新前
<ul>
    <li key="a">a</li>
    <li key="b">b</li>
    <li key="c">c</li>
    <li key="d">d</li>
</ul>
// 更新后
<ul>
    <li key="d">d</li>
    <li key="a">a</li>
    <li key="b">b</li>
    <li key="c">c</li>
</ul>
第一轮遍历开始:

d(后)与a(前)比较,不能复用,跳出第一轮遍历,此时lastPlacedIndex=0;此时新节点包含dabc,老节点包含abcd。属于情况3。老节点保存到map中。

第二轮遍历开始:
  1. d在map中找到,可以复用,oldFiber.index = 3; 由于oldFiber.index > lastPlacedIndex。位置不变,同时把oldFiber.index赋值给lastPlacedIndex。此时lastPlacedIndex=3。
  2. a在map中找到,可以复用,oldFiber.index = 0; 由于oldFiber.index < lastPlacedIndex。标记a移动。lastPlacedIndex不变还是3。
  3. b在map中找到,可以复用,oldFiber.index = 1; 由于oldFiber.index < lastPlacedIndex。标记b移动。lastPlacedIndex不变还是3。
  4. c在map中找到,可以复用,oldFiber.index = 2; 由于oldFiber.index < lastPlacedIndex。标记c移动。lastPlacedIndex不变还是3。

第二轮遍历结束。最终abc三个节点打上Placement标记,可以看到abcd变成dabc,看上去只需要把d移动到最前面,实际上React是保持d不变,将abc分别移动到d后面。所以开发时尽量避免将节点从后面移动到前面。

收尾工作

两次遍历完成后,如果oldFiberMap中还有节点,代表这些节点无法复用,需要打上Deletion标记。

代码实现

// 保留关键代码
// diff数组子节点
function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;
    // 参与比较的current Fiber
    let oldFiber = currentFirstChild;
    // 最后一个可复用oldFiber的位置索引
    let lastPlacedIndex = 0;
    // jsx对象索引
    let newIdx = 0;
    // 下一个oldFiber
    let nextOldFiber = null;
    // 老节点为链表,新节点为虚拟DOM数组
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 1.key相同返回fiber节点,
      //  1.1 如果type相同返回老节点
      //  1.2 type不同返回新节点
      // 2.key不同返回null
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      // newFiber为null意味着当前节点是因为key不同不能复用,立即跳出循环。
      if (newFiber === null) {
        if (oldFiber === null) {
          // 旧节点为null, 恢复为上一次的节点
          oldFiber = nextOldFiber;
        }
        break;
      }
      // 需要追踪副作用
      if (shouldTrackSideEffects) {
        // newFiber.alternate为null说明是新建节点,说明是key相同、type不同创建了新fiber节点
        if (oldFiber && newFiber.alternate === null) {
          // 存在老节点且key相同,但是因为type不同不能复用,给旧节点打上删除标记(具体哪种情况后面在看)
          deleteChild(returnFiber, oldFiber);
        }
      }
      // placeChild很重要
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // resultingFirstChild 保存第一个子节点
        resultingFirstChild = newFiber;
      } else {
        // 后面的节点以链表形式保存
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
    // 第一次循环结束,有三种情况
    // 情况1:新节点遍历完了
    if (newIdx === newChildren.length) {
      // 把剩余的老节点打上删除标记
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    // 情况2:老节点遍历完了
    if (oldFiber === null) {
      // 剩余的新节点都执行新建操作
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }
    // 情况3:新老节点都没遍历完
    // 把老节点创建一个map
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    // 遍历剩余的新节点
    for (; newIdx < newChildren.length; newIdx++) {
      // 判断是否可复用
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) { // 说明是复用的
            // 从map里面删除,避免后面打上删除标记
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      // 两轮循环结束后,map里面的是没有匹配到的节点,打上删除标记
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }
    // 返回第一个子节点
    return resultingFirstChild;
} 

其他方法

useFiber

useFiber方法,调用createWorkInProgress clone一个新的Fiber节点。

// 保留关键代码
// 复用老Fiber
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
    const clone = createWorkInProgress(fiber, pendingProps);
    clone.index = 0;
    clone.sibling = null;
    return clone;
}

updateSlot

// 保留关键代码
function updateSlot(
    returnFiber: Fiber,
    oldFiber: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    const key = oldFiber !== null ? oldFiber.key : null;
    // 新节点是非空文本节点 没有key
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      // 旧节点有key 说明不是文本节点,不能复用
      if (key !== null) {
        return null;
      }
      // 老节点有可能是没有key的非文本节点,所以updateTextNode会继续判断是否可服复用
      return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          if (newChild.key === key) {
            // key相同 判断是否可复用,updateElement会继续判断type是否一致。
            return updateElement(returnFiber, oldFiber, newChild, lanes);
          } else {
            // key不同, 不能复用
            return null;
          }
        }
      }

      if (isArray(newChild) || getIteratorFn(newChild)) {
        if (key !== null) { // 旧节点有key 新节点是数组或者Iterator 不能复用
          return null;
        }
        return updateFragment(returnFiber, oldFiber, newChild, lanes, null); // 忽略Fragment相关
      }
    }
    return null;
}

updateTextNode

判断文本节点是否可复用。

// 保留关键代码
// 处理文本节点
function updateTextNode(
    returnFiber: Fiber,
    current: Fiber | null,
    textContent: string,
    lanes: Lanes,
  ) {
    // 旧节点不存在或者旧节点不是文本节点 新建一个fiber
    if (current === null || current.tag !== HostText) {
      const created = createFiberFromText(textContent, returnFiber.mode, lanes);
      created.return = returnFiber;
      return created;
    } else {
      // 复用旧节点
      const existing = useFiber(current, textContent);
      existing.return = returnFiber;
      return existing;
    }
}

updateElement

// 保留关键代码
function updateElement(
    returnFiber: Fiber,
    current: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const elementType = element.type;
    // 存在有旧节点 继续判断节点类型是否一致
    if (current !== null) {
      if (current.elementType === elementType) {
        const existing = useFiber(current, element.props);
        existing.ref = coerceRef(returnFiber, current, element);
        existing.return = returnFiber;
        return existing;
      }
    }
    // 不存在旧节点,或者节点类型不一样 新建一个fiber
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, current, element);
    created.return = returnFiber;
    return created;
  }

updateFromMap

// 保留关键代码
function updateFromMap(
    existingChildren: Map<string | number, Fiber>,
    returnFiber: Fiber,
    newIdx: number,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      // 文本节点没有key,直接去updateTextNode里面判断,如果都是文本即可复用,否则新建
      const matchedFiber = existingChildren.get(newIdx) || null;
      return updateTextNode(returnFiber, matchedFiber, '' + newChild, lanes);
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          const matchedFiber =
            existingChildren.get(
              newChild.key === null ? newIdx : newChild.key,
            ) || null;
          return updateElement(returnFiber, matchedFiber, newChild, lanes);
        }
      }

      if (isArray(newChild) || getIteratorFn(newChild)) {
        const matchedFiber = existingChildren.get(newIdx) || null;
        return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
      }
    }
    return null;
}

createChild

根据类型创建对应的Fiber节点。

// 保留核心代码
function createChild(
    returnFiber: Fiber,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      // 创建文本节点
      const created = createFiberFromText(
        '' + newChild,
        returnFiber.mode,
        lanes,
      );
      created.return = returnFiber;
      return created;
    }
    // 单节点
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          const created = createFiberFromElement(
            newChild,
            returnFiber.mode,
            lanes,
          );
          created.ref = coerceRef(returnFiber, null, newChild);
          created.return = returnFiber;
          return created;
        }
      }
      // 数组或者是Iterator
      if (isArray(newChild) || getIteratorFn(newChild)) {
        const created = createFiberFromFragment(
          newChild,
          returnFiber.mode,
          lanes,
          null,
        );
        created.return = returnFiber;
        return created;
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }
    return null;
}

deleteRemainingChildren

删除剩余的子Fiber

// 保留关键代码
  function deleteRemainingChildren(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
  ): null {
    // 不需要打标记
    if (!shouldTrackSideEffects) {
      return null;
    }
    let childToDelete = currentFirstChild;
    while (childToDelete !== null) {
      deleteChild(returnFiber, childToDelete);
      childToDelete = childToDelete.sibling;
    }
    return null;
  }

deleteChild

// 保留核心代码
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
    if (!shouldTrackSideEffects) { // 不需要打标记
      return;
    }
    const deletions = returnFiber.deletions; // 父节点上要删除的fiber
    if (deletions === null) {
      returnFiber.deletions = [childToDelete]; // 父节点挂上要删除的子节点
      returnFiber.flags |= ChildDeletion; // 打上删除子节点标记
    } else {
      deletions.push(childToDelete);
    }
}

总结

为了降低Diff算法的复杂度,React做了如下限制。

  1. 只对同级元素进行Diff。如果一个DOM元素在前后两次更新中跨越了层级,React不会尝试复用它。
  2. 两个不同类型的元素会产生不同的树,元素类型变更,React会销毁变更前元素及其子孙元素,并新建更新后元素及其子孙元素。
  3. 开发中可以通过key属性来帮助Diff。
  4. 鉴于在日常开发中,“节点移动”较少,在Diff实现的时候才用了两轮遍历,优先考虑按照顺序复用的大多数情况。

参考

React设计原理 - 卡颂。

转载自:https://juejin.cn/post/7397334706822348800
评论
请登录