Skip to main content

Reconciliation の仕組み

レンダリング中、reconciliation(調整処理)は現在レンダリング中の fiber の最初の子要素を計算して処理するプロセスです。この処理は再帰的にツリーの最下部に到達するまで続き、その後completeWorkが開始されてツリーを遡り始めます。

アプリケーションのライフタイムで最初に reconciliation が呼び出されるのはHostRootをレンダリングする時で、root.render()に渡された次の子要素への遷移が必要になります。

その後も reconciliation は同じメカニズムに従い、常に次の子要素を計算します。

reconcileChildrenの仕組み

reconcileChildrenはこのプロセスのエントリーポイントです:

export function reconcileChildren(
current: Fiber | null, // 現在表示中のfiber
workInProgress: Fiber, // その代替fiber
nextChildren: any, // 次の子要素
renderLanes: Lanes // レンダー用レーン
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}

function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes
) {
thenableIndexCounter = 0;
const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes
);
thenableState = null;
return firstChildFiber;
}

この関数はReactChildFiberモジュールで定義されているreconcileChildFibersを呼び出します。

本セクションの明確化のため、thenable に関する説明は割愛します。

続いて、reconcileChildFibersImplが同じ引数で呼び出されます。

reconcileChildFibersImplの仕組み

この関数は最初に使用する次の子要素を決定します。トップレベルの Fragment で key プロパティがない場合、それをスキップします。

note

これは実際に落とし穴になり得ます!

React は key プロパティがないトップレベルのコンポーネントに対して fiber を作成しません。

tip

関数シグネチャから、reconciliation の目的が実際にはReactNodeを同等のFiberに変換することだと推測できます。

function reconcileChildFibersImpl(
returnFiber: Fiber, // 親fiber
currentFirstChild: Fiber | null, // 現在表示中の最初の子要素のfiber
newChild: any, // 次のReactノード
lanes: Lanes // レンダー用レーン
): Fiber | null {
const isUnkeyedTopLevelFragment =
typeof newChild === "object" && // React要素(配列ではない)
newChild !== null && // typeof nullはobject 🙄
newChild.type === REACT_FRAGMENT_TYPE && // Fragment
newChild.key === null; // keyプロパティ

if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}

if (typeof newChild === "object" && newChild !== null) {
// オブジェクト(React要素、配列など)を処理
}

if (
(typeof newChild === "string" && newChild !== "") ||
typeof newChild === "number"
) {
// テキストノードを処理
}

if (__DEV__) {
if (typeof newChild === "function") {
warnOnFunctionType(returnFiber);
}
}
// 残りのケースはすべて空として扱われる
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

受け取ったnewChildnextChildren)が null ではないオブジェクトの場合、2 つの可能性があります:単一ノードか、複数の子要素のコレクションです。

以下がそれらを区別する方法です:

if (typeof newChild === "object" && newChild !== null) {
// 単一の子要素
switch (newChild.$$typeof) {
}

// 子要素のコレクション
if (isArray(newChild)) {
}
if (getIteratorFn(newChild)) {
}

// 非同期コンポーネント
if (typeof newChild.then === "function") {
}
throwOnInvalidObjectType(returnFiber, newChild);
}

単一子要素の reconciliation の仕組み

有効な$$typeofプロパティを持つ単一の子要素がある場合、以下のケースが処理されます:

switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes)
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes)
);
case REACT_LAZY_TYPE:
const payload = newChild._payload;
const init = newChild._init;
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes
);
}

上記で使用される主要な関数:

  • placeSingleChild
  • reconcileSingleChild
  • reconcileSinglePortal
  • 遅延型の再帰処理:この再帰は主に thenables ケースに該当し、再度 reconciliation に入ります

reconcileSingleElementの仕組み

この関数は同じシグネチャを保持しますが、今回のnewChildパラメータはReactElementであることが明確です:

function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key; // 新しいkey
let child = currentFirstChild;

// 前のツリーをループして同じkeyを持つ要素を検索
while (child !== null) {
// 最初にkeyを比較
if (child.key === key) {
// 次にelementTypeに基づいてロジックを実行
const elementType = element.type; // 新しい要素タイプ
} else {
deleteChild(returnFiber, child);
}

child = child.sibling;
}

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

ご覧の通り、reconcileSingleElementは最初に既存の子要素をループし、同じ key を持つ要素を探します。

見つかった場合、element.typeに対してテストを実行し、Fragment タイプに対して特別な処理パスを持っています。

while (child !== null) {
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;
}
// Fragmentでない場合、deleteChildにフォールスルー
} else {
// 通常の要素(Fragment以外)の場合
if (
// コンポーネントタイプが同じまま
child.elementType === elementType ||
// React.lazyの特別ケース
(typeof elementType === "object" &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}

child = child.sibling;
}

一致が見つかった場合、React はdeleteRemainingChildrenを実行し、既存の fiber をクローンして親(returnFiber)にアタッチし、それを返します。 Fragment は ref を持たないため、coerceRefの部分は存在しません。

deleteRemainingChildrenは、指定された要素から開始して return fiber の子要素配列からすべての fiber を削除します。

この削除プロセスは、削除された fiber を親のdeletionsプロパティに追加します:

// simplified
function deleteRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber | null
): null {
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}

function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}

前のツリーを処理しても一致が見つからなかった場合(マウント時には現在のツリーが存在しないため、最初からここにジャンプします)、React は与えられた React 要素から新しい Fiber を作成して返します。

if (element.type === REACT_FRAGMENT_TYPE) {
const fiber = createFiberFromFragment(
element.props.children, // props
returnFiber.mode, // mode
lanes, // レンダー用レーン
element.key // key
);
fiber.return = returnFiber;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}

Fiber の作成については次のセクションで詳しく説明し、各タイプごとの処理方法を見ていきます。

coerceRef

How reconcileSinglePortal works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

placeSingleChildの仕組み

placeSingleChild関数は、最初に構築された時(代替 fiber がない場合)にPlacementフラグを fiber にマークします。

if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement | PlacementDEV;
}
return newFiber;

How reconcile lazy works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How children array reconciliation works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How reconcileChildrenArray works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How reconcileChildrenIterator works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How text nodes reconciliation works

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.