React のソース コードの解釈と原則 (6): reconcileChildren と DIFF アルゴリズム

コラム冒頭に記載(スタックアーマー)

  1. 著者はフロントエンド技術の専門家ではなく、新しいことを学ぶのが好きな単なるフロントエンド技術の初心者であり、急速に減少するフロントエンド市場と検索の必要性に対処するためだけにソースコードを学びたいと考えています。このコラムは学習の過程での筆者の考えや経験であり、他のチュートリアルを参照している部分も多い. 誤りや問題があれば, 著者に指摘してください. 著者はその内容を保証します.は 100% 正しいです。この列を参考回答として使用しないでください。

  2. このコラムを読むには、React、JavaScript、およびフロントエンド エンジニアリングのある程度の基礎が必要です.babel とは何か、jsx の構文とは何か、などの基本的な知識の多くは、著者は説明しませんので、参照してください。必要に応じて材料。

  3. このコラムの多くの部分は、多数の他のチュートリアルを参照しています. 類似点がある場合は、著者がそれらを盗用しました. したがって、このチュートリアルは完全にオープンソースです. 著者として、さまざまなチュートリアルに独自の理解を統合し、要約し、追加することができます.

このセクションの内容

このセクションでは、前のセクションで残された質問から始めて、最終的にreconcileChildren()でファイバー ノードを生成する方法について説明します. その中で、React のコアであるDIFF アルゴリズム、そのコア - 方法について説明します。多重化を実現すると同時に、比較の時間の複雑さをどのように最適化したか

和解する子供たち

前のセクションで、beginWork 最後にreconcileChildren この関数を呼び出して処理すると述べました。reconcileChildrenこの関数は、おなじみの DIFF アルゴリズムを呼び出して要素を処理し、ファイバーを生成するため、コードのこの部分にあるこの関数から始めます。 :

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.js

これがその主なロジックです. 実際には, それが最初のレンダリングであるかどうかに応じて異なる関数を呼び出すことです.current === nullここでの.

export function reconcileChildren(current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes) {
    
    
  if (current === null) {
    
    
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    
    
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

これら 2 つの関数の定義を見てみましょう. どちらもChildReconcilerfunction、渡されるパラメーターは異なります。

export const reconcileChildFibers = ChildReconciler(true); 
export const mountChildFibers = ChildReconciler(false); 

このChildReconciler関数。そのコードは非常に長いです。最初に戻り値を見てみましょう。reconcileChildFibers

function ChildReconciler(shouldTrackSideEffects) {
    
    
  return reconcileChildFibers;
}

reconcileChildFibersChildReconcilerfunction内で定義される関数であり、そのロジックは次のとおりです。

  • まずfragment要素かどうか判断し、そうであればその子を使う、FragmentコンポーネントはReact 16.2の新機能で、追加のDOM要素を作らずにrender()メソッドで複数の要素を返すことができるので、それを処理するとき、このタグを無視する必要があります
  • 続いて受信ノードを処理します. オブジェクトであれば要素要素であり, 通常の要素, Lazy 型, Portal 型, ノード配列, その他の 5 種類に分けられます. 文字列や数値ノードが渡された場合. 、次にテキストノードとして扱う
  • 着信コンテンツがどのコンテンツとも一致しない場合、ブール値、null、未定義な​​どの可能性があり、ファイバー ノードに変換できないことに注意してください。
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes, 
  ): Fiber | null {
    
    
    // 判断是不是 fragment
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
        
    if (isUnkeyedTopLevelFragment) {
    
    
      newChild = newChild.props.children;
    }
    // 判断该节点的类型
    if (typeof newChild === 'object' && newChild !== null) {
    
    
      switch (newChild.$$typeof) {
    
    
        case REACT_ELEMENT_TYPE:
          // 一般的React组件,
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          // portal类型 
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_LAZY_TYPE:
          // lazy类型 
          const payload = newChild._payload;
          const init = newChild._init;
          return reconcileChildFibers(
            returnFiber,
            currentFirstChild,
            init(payload),
            lanes,
          );
      }
	  // newChild 是一个数组
      if (isArray(newChild)) {
    
    
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
	  // 其他迭代类型,跟数组类似,只是遍历方式不同
      if (getIteratorFn(newChild)) {
    
    
        return reconcileChildrenIterator(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
      throwOnInvalidObjectType(returnFiber, newChild);
    }
    // 文本节点
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
    
    
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }

    if (__DEV__) {
    
    
      if (typeof newChild === 'function') {
    
    
        warnOnFunctionType(returnFiber);
      }
    }

    //说明 newChild 可能是boolean, null, undefined等类型,不能转为fiber节点。直接从删除所有旧的子 Fiber (不继续比较了)
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

関数 reconcileChildFibers は、前のセクションで配置した要素構造を処理します. これは、この要素のコアを処理し、それを最初のレイヤーの要素と比較して、ルールに従ってノードを再利用できるかどうかを判断します.ファイバーノードにはシビングポインターが含まれています。シビングポインターを介して、ノードのすべての兄弟を見つけることができるため、同じレイヤーの要素全体をトラバースします。

比較プロセスを詳しく見てみましょう。

reconcileSingleElement

reconcileSingleElement関数コンポーネント、クラスコンポーネント、htmlタグなど、一般的なReactコンポーネントを処理するために使用されます.コードを見てみましょう.彼の判断ロジックは次のとおりです.

  • まず現在の要素のキー属性を抽出し、それと同じキーを持つ要素がファイバーツリーの同じ層にあるかどうかを調べる
  • 次に、同じキーを持つ要素が現在の要素と同じ型であるかどうかを判断し、そうであれば要素を再利用します。
  • 一致する要素がない場合は、新しいノードを作成します
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
    
    
  //当前节点的key
  const key = element.key;
  let child = currentFirstChild;
  // 循环检测一层中和当前节点的 key
  while (child !== null) {
    
    
    // 找到 key 相等的元素
    if (child.key === key) {
    
    
      const elementType = element.type;
      //元素类型相等
      if (child.key === key) {
    
    
        const elementType = element.type;
         //  REACT_FRAGMENT_TYPE,特判
        if (elementType === REACT_FRAGMENT_TYPE) {
    
    
          if (child.tag === Fragment) {
    
    
            deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用Fiber子节点且确认只有一个子节点,因此标记删除掉该child节点的所有sibling节点
            const existing = useFiber(child, element.props.children); // 该节点是fragment类型,则复用其children
            existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
            //Fragment没有 ref属性
            if (__DEV__) {
    
    
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            return existing;
          }
        } else {
    
    
          if (
            child.elementType === elementType ||
            (__DEV__
              ? isCompatibleFamilyForHotReloading(child, element)
              : false) ||
            (typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
    
    
            deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用Fiber子节点且确认只有一个子节点,因此标记删除掉该child节点的所有sibling节点
            const existing = useFiber(child, element.props); // 复用 child 节点和 element.props 属性
            existing.ref = coerceRef(returnFiber, child, element); // 处理ref
            existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
            if (__DEV__) {
    
    
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            return existing;
          }
        }
      // key一样,类型不同,直接删除该节点和其兄弟节点
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
    
    
      // 若key不一样,不能复用,标记删除当前单个child节点
      deleteChild(returnFiber, child);
    }
    // 指针指向下一个sibling节点
    child = child.sibling; 
  }
  // 创建一个新的fiber节点
  if (element.type === REACT_FRAGMENT_TYPE) {
    
    
    //  REACT_FRAGMENT_TYPE,特判
    const created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);
    created.return = returnFiber; // 新节点的 return 指向到父级节点
    // FRAGMENT 节点没有 ref
    return created;
  } else {
    
    
    // 普通的html元素、函数组件、类组件等
    // 从 element 创建 fiber 节点
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element); // 处理ref
    created.return = returnFiber;
    return created;
  }
}

ノードを再利用すると useFiber 関数が呼び出されることに気付きました。この関数をもう一度見てみましょう。

彼はファイバー ノードを呼び出してcreateWorkInProgressクローンを作成し、それを WorkInProgress ツリーに配置しました. React には 2 つのツリーがあり、FiberRootNode ノードの現在のポインターがどちらのツリーを指しているかを前に説明しましたが、そのツリーを表示します.現在表示されているツリーは current と呼ばれ、構築されるツリーは workInProgress と呼ばれ、alternate 属性を介して相互にポイントします。ここでは、現在のツリーを使用して workInProgress ツリーを作成します。

function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
    
    
  // 调用了 createWorkInProgress 这个函数来克隆一个节点
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
    
    
  let workInProgress = current.alternate;
  // workInProgress 是空的,也就是初始化的时候,创建一个新节点
  if (workInProgress === null) {
    
    
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    // workInProgress 和 current通过 alternate 属性互相进行指向
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    
    
    workInProgress.pendingProps = pendingProps;
    workInProgress.type = current.type;
    workInProgress.flags = NoFlags;
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;

    if (enableProfilerTimer) {
    
    
      workInProgress.actualDuration = 0;
      workInProgress.actualStartTime = -1;
    }
  }

  workInProgress.flags = current.flags & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;

  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
    
  const currentDependencies = current.dependencies;
  workInProgress.dependencies =
    currentDependencies === null
      ? null
      : {
    
    
          lanes: currentDependencies.lanes,
          firstContext: currentDependencies.firstContext,
        };

  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  if (enableProfilerTimer) {
    
    
    workInProgress.selfBaseDuration = current.selfBaseDuration;
    workInProgress.treeBaseDuration = current.treeBaseDuration;
  }

  return workInProgress;
}

最後に、createFiberFromElement関数要素からファイバーを作成し、この関数createFiberFromElementが処理のために関数を呼び出していることがわかります。この関数のロジックを見てみましょう。

  • 最初に type 属性に基づいて、コンポーネント、html ノード、およびその他のタイプのコンポーネントを識別します。
  • コンポーネントであれば、クラスコンポーネントは React.Component を継承する必要があり、React.Component のプロトタイプに isReactComponent 属性があり、値が {} であるため、 に従って関数コンポーネントかクラスコンポーネントかshouldConstructを判断します。判断できるように
  • 次にcreateFiber関数ファイバ ノードを作成します. この関数は前回のチュートリアルで使用しました. 忘れた読者は前回のチュートリアルを読んでください. 作成されたファイバを取得したら, タイプなどの属性を割り当てて返します.
  • この作成されたファイバーは、最終的にツリーに配置されます
export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
    
    
  let owner = null;
  if (__DEV__) {
    
    
    owner = element._owner;
  }
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props; 
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    lanes,
  );
  return fiber;
}

export function createFiberFromTypeAndProps(
  type: any, 
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
    
    
  let fiberTag = IndeterminateComponent; // 我们还不知道当前fiber是什么类型
  let resolvedType = type;
  if (typeof type === 'function') {
    
    
    // 当前是函数组件或类组件
    if (shouldConstruct(type)) {
    
    
      fiberTag = ClassComponent;
    } else {
    
    
      // 函数组件
    }
  } else if (typeof type === 'string') {
    
    
    // type是普通的html标签
    fiberTag = HostComponent;
  } else {
    
    
    // 其他类型,按下不表
  }
  // 调用 createFiber() 函数,生成 fiber 节点
  const fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type; // fiber中的 elmentType 与 element 中的 type 一样,
  fiber.type = resolvedType;
  fiber.lanes = lanes;
  return fiber;
}

function shouldConstruct(Component: Function) {
    
    
  // 类组件都是要继承 React.Component 的,而 React.Component 的 prototype 上有一个 isReactComponent 属性,值为{}
  const prototype = Component.prototype;
  return !!(prototype && prototype.isReactComponent);
}

reconcileSingleTextNode

純粋な html テキストの場合, 処理は比較的簡単です. もちろん, マルチテキスト配列と単一ノードに分割することもできます. ここでは単一ノードの処理について説明します. 配列型の処理は複数の ReactElement 要素の処理 次に、詳細を説明します。

彼の処理reconcileSingleTextNodeこの、テキスト ノードにはキー属性がないため、キーを決定する必要がなくなりました。現在のレイヤーのデータを比較すると、テキスト ノードにはノードが 1 つしかなく、兄弟ノードがなく、最初のノードのみがあるためです。ノードがテキスト ノードの場合のみ再利用できます。それ以外の場合は、すべての要素が削除されます。

// 调度文本节点
function reconcileSingleTextNode(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  textContent: string,
  lanes: Lanes,
): Fiber {
    
    
  // 不再判断文本节点的key,因为文本节点就来没有key
  if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
    
    
    // 若当前节点是文本,则直接删除后续的兄弟节点
    deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
    const existing = useFiber(currentFirstChild, textContent); // 复用这个文本的fiber节点,重新赋值新的文本
    existing.return = returnFiber;
    return existing;
  }
  // 若不存在子节点,或者第一个子节点不是文本节点,直接将当前所有的节点都删除,然后创建出新的文本fiber节点
  deleteRemainingChildren(returnFiber, currentFirstChild);
  const created = createFiberFromText(textContent, returnFiber.mode, lanes);
  created.return = returnFiber;
  return created;
}

reconcileChildrenArray

処理する必要がある要素が単一のデータではなく、3 つの p タグでネストされた div などの一連のデータである場合、これはより一般的です。処理reconcileChildrenArray

配列内の要素の更新には、次の状況が発生する可能性があることが知られています。

  • 新しいシーケンスと古いシーケンスを比較すると、要素は同じ位置に表示されます
  • 新しいシーケンスに要素を追加しました
  • 要素は新しいシーケンスから削除されます
  • 新しいシーケンスと古いシーケンスの両方に出現するが、順序が異なる要素

最初に、Fiber リンク リストと配列を順番にトラバースします。これは、Fibre リンク リストが sibing ポインターを介して次のノードを指しているからですが、前のノードに戻るポインターがないため、リンク リストを前から次へとトラバースすることしかできません。戻る, 同時に, 見てみましょう. ファイバーには、ファイバー兄弟リスト内の要素の位置を示すインデックスという属性があります. この属性は、ここで役立ちます:

let resultingFirstChild: Fiber | null = null; // 用于返回的链表
let previousNewFiber: Fiber | null = null; 

let oldFiber = currentFirstChild; // 旧 Fiber 链表的节点,开始指向同一层中的第一个节点
let lastPlacedIndex = 0; // 表示当前已经新建的 Fiber 长度
let newIdx = 0; // 表示遍历 newChildren 的索引指针
let nextOldFiber = null; // 下一个 fiber 节点

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    
    
  // 如果旧的节点大于新的
  if (oldFiber.index > newIdx) {
    
    
    nextOldFiber = oldFiber;
    oldFiber = null;
  } else {
    
    
    // 旧 fiber 的索引和n ewChildren 的索引匹配上了,获取 oldFiber 的下一个兄弟节点
    nextOldFiber = oldFiber.sibling;
  }

  // 比较旧的节点和将要转换的 element 
  const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
  // 匹配失败,不能复用
  if (newFiber === null) {
    
    
    if (oldFiber === null) {
    
    
      oldFiber = nextOldFiber;
    }
    break;
  }
  if (shouldTrackSideEffects) {
    
    
    if (oldFiber && newFiber.alternate === null) {
    
    
      // newFiber 不是基于 oldFiber 的 alternate 创建的,销毁旧节点
      deleteChild(returnFiber, oldFiber);
    }
  }
  // 更新lastPlacedIndex
  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

  // 更新返回的链表
  if (previousNewFiber === null) {
    
    
    // 若整个链表为空,则头指针指向到newFiber
    resultingFirstChild = newFiber;
  } else {
    
    
    // 若链表不为空,则将newFiber放到链表的后面
    previousNewFiber.sibling = newFiber;
  }
  previousNewFiber = newFiber; 
  oldFiber = nextOldFiber; // 继续下一个节点
}

updateSlotこの関数は、対応する同じ位置にある 2 つの要素を再利用できるかどうかを判断するために使用され、その判断の基準は、2 つの要素のキーが同じかどうかです。

function updateSlot(returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, lanes: Lanes): Fiber | null {
    
    
  // 若key相等,则更新fiber节点;否则直接返回null
  const key = oldFiber !== null ? oldFiber.key : null;
  if ((typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number') {
    
    
    // 文本节点本身是没有key的,若旧fiber节点有key,则说明无法复用
    if (key !== null) {
    
    
      return null;
    }
    // 若旧fiber没有key,即使他不是文本节点,我们也尝试复用
    return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
  }
  if (typeof newChild === 'object' && newChild !== null) {
    
    
    // 若是一些ReactElement类型的,则判断key是否相等;相等则复用;不相等则返回null
    switch (newChild.$$typeof) {
    
    
      case REACT_ELEMENT_TYPE: {
    
    
        if (newChild.key === key) {
    
    
          // key一样才更新
          return updateElement(returnFiber, oldFiber, newChild, lanes);
        } else {
    
    
          // key不一样,则直接返回null
          return null;
        }
      }
      // 省略....
    }
    if (isArray(newChild) || getIteratorFn(newChild)) {
    
    
      // 当前是数组或其他迭代类型,本身是没有key的,若oldFiber有key,则无法复用
      if (key !== null) {
    
    
        return null;
      }
      // 若 newChild 是数组或者迭代类型,则更新为fragment类型
      return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
    }
  }
  // 其他类型不进行处理,直接返回null
  return null;
}

ループが終了した後、古いリンクされたリストがトラバースされていない場合、残りのノードは不要になり、直接削除できることを意味します。

// 遍历结束(访问相同数量的元素了)
if (newIdx === newChildren.length) {
    
    
  // 删除旧链表中剩余的节点
  deleteRemainingChildren(returnFiber, oldFiber);
  // 返回新链表的头节点指针
  return resultingFirstChild;
}

上記の操作の後、古いファイバーが使い果たされたが、要素要素がすべてアクセスされていない場合、残りの要素には再利用できる対応するノードがないことを意味します. 新しいノードを直接作成するだけです. ロジックは基本的に使用したcreateChild質問と同じ:updateSlot

// 若旧数据中所有的节点都复用了
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;
}

新しい要素と古い要素がトラバースされていない場合は、要素が順不同であることを意味します.古いノードをマップに配置し、キーまたはインデックスに従ってそれらを取得する必要があります.

//如果新旧元素都没遍历完, mapRemainingChildren 生成一个以 oldFiber 的 key 为 key, oldFiber 为 value 的 map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

for (; newIdx < newChildren.length; newIdx++) {
    
    
  // 从 map 中查找是否存在可以复用的fiber节点,然后生成新的fiber节点
  const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
  if (newFiber !== null) {
    
    
    if (shouldTrackSideEffects) {
    
    
      if (newFiber.alternate !== null) {
    
    
         //newFiber.alternate指向到current,若current不为空,说明复用了该fiber节点,这里我们要在 map 中删除,因为后面会把 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 中没有复用的 fiber 节点添加到删除队列中,等待删除
  existingChildren.forEach(child => deleteChild(returnFiber, child));
}
// 返回新链表的头节点指针
return resultingFirstChild;

差分アルゴリズム

reconcileChildren 行われた。次に、その処理ロジックを整理して、ファイバー ツリーを更新するためのアルゴリズムに抽象化しましょう。このアルゴリズムは React の DIFF アルゴリズムです. まず、React の DIFF の大前提について話しましょう:

  • Web 開発では、レベルをまたがる要素の変更はほとんどありません.たとえば、今 p タグがあります.私がよく行うことは、このタグの内容を変更するか、p タグと同じレベルに別の p タグを挿入することです.ページを更新するときに、p タグの周りに div をネストすることはめったにありません。
  • 2 つの異なるタイプのコンポーネントは、2 つの異なるツリー構造を生成します
  • React のキーは一意であり、キーは要素を一意に識別できます

この前提に基づいて、React によって与えられる戦略は次のとおりです。

React は仮想 DOM ツリーの階層比較のみを実行し、ノードのクロスレベル比較は考慮しません。このように、仮想 Dom ツリーを 1 回トラバースするだけで、全体の比較を完了することができます。比較するときは、毎回同じ親ノードの子を選択して比較します。下の図に示すように、同じ親ノードの下のすべての子が比較されます。

写真の説明を追加してください

階層をまたがる移動がある場合、親ノードを比較すると、階層をまたがって移動したノードが削除されます.たとえば、下の図では、ルートを比較すると、A が削除され、次に B が削除されています。比較すると、コンポーネントAが表示されますが、再利用せず、新しいコンポーネントAを作成します

ここに画像の説明を挿入

異なるタイプのコンポーネントの場合、React はデフォルトで比較操作を実行する必要がなく、それらを直接再作成します。同じタイプのコンポーネントについては、下の図に示すように、比較に diff 戦略を使用します。2 つのコンポーネントのルート ノードは異なります。つまり、コンポーネントではありませんが、コンポーネントのコンテンツはこの場合、React は再利用しませんが、新しいものを直接作成します。

ここに画像の説明を挿入

同じレイヤーのノードの場合、一意のキーを使用して同じノードが古いコレクションに存在するかどうかを判断し、存在しない場合は作成し、存在する場合は移動する必要があるかどうかを判断します。操作全体は、削除、追加、移動の3 つのタイプに分けられます。下の図に示すように、A を移動し、C を削除し、E を追加しました。

ここに画像の説明を挿入
ここに画像の説明を挿入

上記のロジックの最適化の後、React は従来の DIFF アルゴリズム (激しい再帰的比較) の O( n^3 ) をほぼ O( n ) に最適化します。

これで、説明したソース コードを確認できます。これで、ソース コードについて新たな理解が得られ、ほとんどの人が初めてソース コードを読んだときに明確にならない問題のいくつかも解決されました。

  • ノードを処理するときに解析できない場合 (null、未定義)、すべての要素を直接削除する必要があるのはなぜですか。古い要素 同じレイヤーのものは価値がありません。すべて削除してください。

  • ノードを処理するときに再利用できるノードを見つけたときに、残りのノードを削除する必要があるのはなぜですか? 単一のノードを処理している場合、同じレイヤーで、再利用するノードは 1 つしか必要ないためです。再利用できるノード, 残りのノードは必要ありません.直接削除してください.

  • キーが同じでタイプが異なる場合、キーは一意であるため、同じキーでタイプが異なる要素を見つけて、すべての要素を直接削除するの2 つの異なるツリー構造の原則を生成します。それらを直接削除するだけです

  • ここで追加する必要があるのは、配列型の要素を扱う場合、使用する比較方法は、最初にノードを再利用し、次に削除、追加、および移動を処理することです。これは、実際の React 開発プロセスではノードを再利用するため、その頻度追加と削除は移動よりもはるかに高く、最後に移動するロジックを処理します

要約する

このセクションでは、 DIFFアルゴリズムを使用して再利用できるノードを決定するreconcileChildren()関数の処理について説明しました

  • 同じレベルの要素のみを比較する
  • ルートノードが異なる場合、ツリー全体が異なると見なされます
  • 一意のキーを使用して要素を識別し、再利用できるかどうかを調べます

この点で、単一のノードの場合、同じレイヤーに再利用可能なノードがあるかどうかを比較します; ノードのグループの場合、同じレイヤーで比較して、各要素を再利用、削除、追加、または移動する必要があるかどうかを判断します。

判定終了後、React要素ノードごとにファイバーノードを作成し、ファイバーのcreateWorkInProgressクローン新しいファイバーを作成し、新しく生成されたファイバーをWorkInProgressツリーに配置し、現在表示されているツリーを配置しますそれは と呼ばれcurrent、構築されるものは と呼ばれworkInProgress、代替属性を介して相互にポイントします。

次に、次の章では、対処する最後のステップのみを示します。つまり、仮想 DOM の更新を実際の DOM に同期し、それをユーザーに表示する必要があります。その後、React レンダリングが完了します。この段階は次のとおりです。これはコミット段階と呼ばれ、次のセクションで詳しく説明します。

おすすめ

転載: blog.csdn.net/weixin_46463785/article/details/130085962