React は内部的にどのように再レンダーするのか?
React Internals Deep Dive シリーズのエピソード 3。Youtube で解説しています
React@18.2.0 ベース。新しいバージョンでは実装が変更されている可能性があります
初期マウント時の React の動作について説明しました。初期マウント後、React は reconciliation プロセスを通じて DOM を可能な限り再利用しようとします。このエピソードでは、デモのボタンクリック後の再レンダー処理の内部動作を解説します。
デモは前回のエピソードとは異なります。こちらで試すことができます。
目次
- 1. トリガーフェーズでの再レンダー
- 2. レンダーフェーズでの再レンダー
- 2.1 基本的なレンダリングロジックは初期マウント時と同じ
- 2.2 React は新しい Fiber Node 作成前に既存のものを再利用
- 2.3
beginWork()
の Update ブランチ - 2.4
attemptEarlyBailoutIfNoScheduledUpdate()
内の Bailout ロジック - 2.5
memoizedProps
vspendingProps
- 2.6
updateFunctionComponent()
による関数コンポーネントの再レンダーと children の reconcile - 2.7
reconcileSingleElement()
による単一要素の調整 - 2.8 コンポーネントが再レンダーされると、そのサブツリーはデフォルトで再レンダーされる
- 2.9
updateHostComponent()
によるホストコンポーネントの更新 - 2.10
reconcileChildrenArray()
が必要に応じて fiber を作成・削除 - 2.11
placeChild()
とdeleteChild()
による Fiber フラグのマーキング - 2.12
updateHostText()
によるテキストノードの更新 - 2.13
completeWork()
による HostComponent の更新マーキングと DOM ノード作成
- 3. コミットフェーズでの再レンダー
- 4. サマリー
1. トリガーフェーズの再レンダー
React は初期マウントで Fiber Tree と DOM ツリーを構築します。完了したとき、以下の 2 つのツリーが存在します。
1.1 lanes
と childLanes
Lane は保留中の work の優先度を表します。Fiber Node は以下を持ちます:
lanes
=> 自身の保留中の work 用childLanes
=> サブツリーの保留中の work 用
Lanes の詳細は What are Lanes in React source code を参照
ボタンがクリックされるとsetState()
が呼び出されます:
- root からターゲット fiber までのパスに
lanes
とchildLanes
がマークされ、次回のレンダーでチェックが必要な箇所を示す scheduleUpdateOnFiber()
によってアップデートがスケジュールされ、最終的にensureRootIsScheduled()
が呼び出されて Scheduler にperformConcurrentWorkOnRoot()
が登録されます。これは初期マウントと同様のプロセスです。
重要な点は、イベントの優先度がアップデートの優先度を決定することです。click
イベントはDiscreteEventPriority
であり、SyncLane
(高優先度)にマッピングされます。
useState()
の動作詳細は How does useState() work internally in React を参照
詳細は割愛しますが、最終的に作業対象となる以下の Fiber Tree が得られます。
2. レンダーフェーズでの再レンダー
2.1 基本的なレンダリングロジックは初期マウント時と同じ
click
イベントの場合、レンダーレーンは SyncLane であり、これはブロッキングレーンです。
したがって、初期マウントと同様に、performConcurrentWorkOnRoot()
内で concurrent mode はまだ有効になっていません。
concurrent mode が有効になっている場合は、How does useTransition() work internally in React?を参照
以下は全体のプロセスをまとめたコードです。
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
next = beginWork(current, unitOfWork, subtreeRenderLanes);
// ~ この行は重要です。[2.5 memoizedProps vs pendingProps](#25-memoizedprops-vs-pendingprops)で説明します
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 新しいworkが生成されない場合、現在のworkを完了
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
詳細な説明については、前回のエピソードを参照してください。 ここでは、React が Fiber Tree をトラバースし、必要に応じて fiber を更新することを覚えておいてください。
2.2 React は新しい Fiber Node 作成前に既存のものを再利用
初期マウントでは、Fiber がスクラッチから作成されます。 しかし実際には、React は最初に Fiber Node を再利用しようとします。
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
// ~ currentは現在のバージョン
// ~ alternateは以前のバージョンを指す
if (workInProgress === null) {
// ~ 最初から作成する必要がある場合
// ツリーのバージョンは最大でも 2 つしか必要ないことがわかっているため、
// ダブル バッファリングのプーリング手法を使用します。
// ここで、再利用できる「その他の」未使用ノードをプールします。
// 更新されないものに対して余分なオブジェクトを割り当てないように、遅延して作成されます。
// また、必要に応じて余分なメモリを再利用することもできます。
workInProgress = createFiber(
current.tag,
pendingProps,
// ~ ------
current.key,
current.mode,
);
...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// ~ 前のバージョンを再利用可能な場合
workInProgress.pendingProps = pendingProps;
// ~ 再利用可能なため、新しいFiber Nodeを作成する必要はありません
// ~ 代わりに必要なプロパティを更新して再利用します
// Blocksがtypeにデータを保存するため必要
workInProgress.type = current.type;
// alternateは既に存在
// effect tagをリセット
workInProgress.flags = NoFlags;
// エフェクトは無効化
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
// 静的エフェクト以外をリセット
// 静的エフェクトは特定のレンダーに紐づきません
workInProgress.flags = current.flags & StaticMask;
// ~ lanesとchildLanesをコピー
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
...
return workInProgress;
}
current
が Fiber Tree 上にないすべての Fiber Node は再利用できます。
再レンダープロセスでは、冗長なHostRoot
はprepareFreshStack()
で再利用されます。
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
...
workInProgressRoot = root;
// ~ ルートの`current`はHostRootのFiberNodeです
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
したがって、再レンダリングを以下のヘッドから開始します。
色を付けてみましょう。
2.3 beginWork()
の Update ブランチ
beginWork()
内には、更新を処理する重要なブランチがあります。
このブランチは、初期マウントのエピソードでは扱いませんでした。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// ~ currentがnullでない場合、初期マウントではなく
// ~ 以前のバージョンのFiber NodeとDOMノード(HostComponentの場合)が存在する
// ~ これによりReactはサブツリーの深い処理をスキップ(bailout)して最適化可能
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
// ~ ここではshallow equalではなく`===`を使用
// ~ これがReactレンダリングの重要な挙動につながる
hasLegacyContextChanged() ||
// ホットリロードによる実装変更時は強制再レンダー
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// propsまたはcontextが変更された場合、workを実行済みとしてマーク
// memo化された場合は後で解除される可能性あり
didReceiveUpdate = true;
} else {
// propsもlegacy contextも変更なし。保留中の更新または
// context変更をチェック
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
// ~ fiberの`lanes`をチェック
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// エラーやサスペンス境界の2回目のパスでは
// `current`にスケジュールされたworkがない可能性あり
(workInProgress.flags & DidCapture) === NoFlags
) {
// 保留中の更新やcontextなし。即時bailout
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
// ~ このfiberに更新がない場合、Reactはbailoutを試みる
// ~ propsやcontext変更がない場合のみ
current,
workInProgress,
renderLanes,
);
}
...
}
} else {
didReceiveUpdate = false;
// ~ これは以前説明したマウント時のブランチ
...
}
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
//------------------------
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
//--------------
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
//-------------------
case HostText:
return updateHostText(current, workInProgress);
//--------------
...
}
}
2.4 attemptEarlyBailoutIfNoScheduledUpdate()
内の Bailout ロジック
この関数は、不要な場合はできるだけ早くレンダリングを停止しようとします。
function attemptEarlyBailoutIfNoScheduledUpdate(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// このfiberには保留中のworkがありません。beginフェーズに入らずにbailout
// 最適化されたパスでもいくつかの管理作業が必要です(主にスタックへのプッシュ)
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
if (enableCache) {
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
}
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
break;
...
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
//----------------------------
}
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
if (current !== null) {
// 以前の依存関係を再利用
workInProgress.dependencies = current.dependencies;
}
// 子要素に保留中のworkがあるかチェック
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// ~ ここで`childLanes`がチェックされる
// 子要素にもworkがない場合、処理をスキップ可能
// TODO: 再開機能を追加する際、子要素がwork-in-progressセットか確認が必要
if (enableLazyContextPropagation && current !== null) {
// bailout前に子要素のcontext変更をチェック
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
// ~ fiber自身とサブツリーに更新がない場合
// ~ ツリーの深い階層への進行を停止
}
} else {
return null;
// ~ fiber自身とサブツリーに更新がない場合
// ~ ツリーの深い階層への進行を停止
}
}
// このfiberにworkはないがサブツリーには存在する場合
// 子fiberをクローンして処理継続
cloneChildFibers(current, workInProgress);
// ~ "clone"と名前が付いているが、実際には新しい子ノードを作成するか
// ~ 以前のノードを再利用する
return workInProgress.child;
// ~ 子fiberを直接返し、Reactは次のfiberとして処理を続行
// ~ 詳細は [Reactが内部的にFiberツリーをトラバースする方法](/docs/TODO) を参照
}
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber
): void {
if (current !== null && workInProgress.child !== current.child) {
throw new Error("Resuming work not yet implemented.");
}
if (workInProgress.child === null) {
return;
}
let currentChild = workInProgress.child;
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
// ~ cloneChildFibers()内では、子fiberは以前のバージョンから作成されます
// ~ ただしreconciliation中に設定された新しいpendingPropsが適用されます
workInProgress.child = newChild;
newChild.return = workInProgress;
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
currentChild.pendingProps
);
newChild.return = workInProgress;
}
newChild.sibling = null;
}
Bailout プロセスをまとめると以下のようになります
- fiber が props/context の変更を持たず、かつ pending work(空の
lanes
)がない場合- 子孫が pending work(空の
childLanes
)を持たない場合、 bailout が発生し、React はツリーのさらに下の階層に進まない - それ以外の場合、React はこの fiber を再レンダーせずに子孫に進む
- 子孫が pending work(空の
- それ以外の場合、React はまずこの fiber を再レンダーし、その後子孫に進む
Bailout の詳細は How does React bailout work in reconciliation を参照
2.5 memoizedProps
vs pendingProps
beginWork()
では、workInProgress
がcurrent
と比較されます。props については、
workInProgress.pendingProps
とcurrent.memoizedProps
が比較されます。memoizedProps
は現在の props、pendingProps
は次のバージョンと考えることができます。
React は Render phase で新しい Fiber Tree を作成し、その後現在の Fiber Tree と比較します。pendingProps
は実際には workInProgress の作成時のパラメータであることがわかります。
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
// ~ currentは現在のバージョン
// ~ alternateは以前のバージョンを指す
if (workInProgress === null) {
// ~ 最初から作成する必要がある場合
// ツリーのバージョンは最大でも 2 つしか必要ないことがわかっているため、
// ダブル バッファリングのプーリング手法を使用します。
// ここで、再利用できる「その他の」未使用ノードをプールします。
// 更新されないものに対して余分なオブジェクトを割り当てないように、遅延して作成されます。
// また、必要に応じて余分なメモリを再利用することもできます。
workInProgress = createFiber(
current.tag,
pendingProps,
// ~ ------------
current.key,
current.mode,
);
...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// ~ 前のバージョンを再利用可能な場合
workInProgress.pendingProps = pendingProps;
// ~ 再利用可能なため、新しいFiber Nodeを作成する必要はありません
// ~ 代わりに必要なプロパティを更新して再利用します
// Blocksがtypeにデータを保存するため必要
workInProgress.type = current.type;
// alternateは既に存在
// effect tagをリセット
workInProgress.flags = NoFlags;
// エフェクトは無効化
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
// 静的エフェクト以外をリセット
// 静的エフェクトは特定のレンダーに紐づきません
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,
};
// 親のreconciliation処理で上書きされるプロパティ
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
workInProgress.refCleanup = current.refCleanup;
return workInProgress;
}
実際には、root FiberNode コンストラクタにpendingProps
がパラメータとして存在します。
function createFiber(
tag: WorkTag,
pendingProps: mixed,
//--------------------
key: null | string,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe[incompatible-call]: 形状は正しいが、Flow はコンストラクタを好まない
return new FiberNode(tag, pendingProps, key, mode);
}
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
//--------------------
key: null | string,
mode: TypeOfMode,
) {
...
}
これは理解できます。Fiber Node の作成は最初のステップです。後で作業する必要があります。
そして、fiber の再レンダリングが完了すると、memoizedProps
がpendingProps
で更新されます。これはperformUnitOfWork()
内で行われます。
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// ~ memoizedPropsはworkが完了した後に更新されます
if (next === null) {
// ~ 新しいworkが生成されない場合、現在のworkを完了
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
デモを見てみましょう。
- React は HostRoot(lanes: 0, childLanes: 1)を処理します。HostRoot には props がなく、
memoizedProps
とpendingProps
はどちらも null であるため、React は直接子であるクローンされたApp
に進みます。 - React は
<App/>
(lanes: 0, childLanes: 1)を処理します。App コンポーネントは再レンダーされないため、memoizedProps
とpendingProps
は同じであるため、React は直接子であるクローンされたdiv
に進みます。 - React は
<div/>
(lanes: 0, childLanes: 1)を処理します。この子供たちは App から来ていますが、App は再実行されないので、どの子供(<Link>
、<br/>
、<Component/>
)も変更されていません。したがって、再び React は<Link/>
に進みます。 - React は
<Link/>
(lanes: 0, childLanes: 0)を処理します。この時点で React はさらに下の階層に進む必要がないので、ここで停止し、兄弟である<br/>
に進みます。 - React は
<br/>
(lanes: 0, childLanes: 0)を処理し、再び bailout が発生し、<Component/>
に進みます。
ここで何かが異なっています。<Component/>
にはlanes
が1
あり、React はその子供を再レンダーして reconcile する必要があります。これはupdateFunctionComponent(current, workInProgress)
によって行われます。
これまでに以下の状態になりました。
2.6 updateFunctionComponent()
による関数コンポーネントの再レンダーと children の reconcile
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes
) {
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
context = getMaskedContext(workInProgress, unmaskedContext);
}
let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);
nextChildren = renderWithHooks(
// ~ これは関数コンポーネントを実行し、子要素を返します
current,
workInProgress,
Component,
nextProps,
context,
renderLanes
);
hasId = checkDidRenderIdHook();
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// React DevToolsはこのフラグを読み取ります
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
// ~ nextChildrenを渡し、reconcileChildren()を呼び出します
return workInProgress.child;
}
reconcileChildren()
はReact が初期マウントを行う方法で内部的にいくつかのバリエーションがあり、children のタイプに応じて異なります。3 つに焦点を当てます。
新しい子 fiber を作成することに加えて、既存の fiber を再利用しようとします。
function reconcileChildFibersImpl(
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 placeSingleChild(
//If just a single child
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
...
case REACT_LAZY_TYPE:
...
}
if (isArray(newChild)) {
return reconcileChildrenArray(
// childrenが要素の配列の場合
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
...
}
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
// childrenがテキストの場合
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// 残りのケースはすべて空として扱われます
return deleteRemainingChildren(returnFiber, currentFirstChild);
//-----------------------
}
<Component/>
は単一のdiv
を返します。したがって、reconcileSingleElement()
に進みます。
2.7 reconcileSingleElement()
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
// ~ これはComponent()の戻り値、`<div/>`の要素です
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// TODO: key === nullかつchild.key === nullの場合、リストの最初のアイテムにのみ適用
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
...
} else {
if (
child.elementType === elementType ||
// ~ タイプが同じ場合、再利用可能
// ~ それ以外の場合はdeleteChild()を実行
// このチェックはfalseパスでのみ実行されるようインライン化
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazyタイプは解決後のタイプでreconcileする必要あり
// ホットリロードチェックの後で実行する必要がある
// ホットリロードは再サスペンドせず、prodとは異なるセマンティクスを持つため
// 以下の呼び出しをサスペンドさせることはできない
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
// ~ 既存のfiberを新しいpropsで再利用する試み
// ~ element.propsは`<div/>`のprops
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
}
// マッチしなかった場合
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
...
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
useFiber
内では、React は以前のバージョンを新規作成または再利用します。
前述の通り、子要素を含むpendingProps
が設定されます。
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
// シングルチャイルドの場合のreturn忘れを防ぐため、siblingをnullにしindexを0に設定
const clone = createWorkInProgress(fiber, pendingProps);
//-----------------------------------------
clone.index = 0;
clone.sibling = null;
return clone;
}
コンポーネントが再レンダーされた後、React は新しい<div/>
である子要素に移動します。
この現在のバージョンは空のlanes
とchildLanes
を持っています。
2.8 コンポーネントが再レンダーされると、そのサブツリーはデフォルトで再レンダーされる
<div/>
とその children に scheduled work がない場合、bailout が発生すると考えがちですが、実際にはそうなりません。
beginWork()
内で行われるmemoizedProps
とpendingProps
のチェックを思い出してください。
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
//---------------------
hasLegacyContextChanged() ||
// ホットリロードによる実装変更時は強制再レンダー
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// propsまたはcontextが変更された場合、workを実行済みとしてマーク
// もしpropsが後から等しいと判明した場合、後で解除される可能性あり
didReceiveUpdate = true;
}
ここで重要なのは、props の比較に shallow equal が使用されていない点です。
コンポーネントがレンダーされるたびに、React 要素を含む新しいオブジェクトが生成されるため、pendingProps
は毎回新しく作成されます。
<div/>
の場合、Component()
が実行されると常に新しい props が取得されるため、bailout は発生しません。
したがって React は update ブランチ(updateHostComponent()
)に進みます。
2.9 updateHostComponent()
Props : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
// ホストノードの直接のテキストなchildを特別扱いします。これは一般的なケースです。
// 具象化された子として扱わず、代わりにこのpropにアクセスできるホスト環境で処理します。
// これにより別のHostText fiberの割り当てと走査を回避できます。
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// 直接のテキストchildから通常の子、または空への切り替え時、
// テキストコンテンツのリセットをスケジュールする必要があります
workInProgress.flags |= ContentReset;
}
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
//---------------------------------------------------------------------
return workInProgress.child;
}
nextChildren
here is:
[
{ $$typeof: Symbol(react.element), type: "button" },
" (",
{ $$typeof: Symbol(react.element), type: "b" },
")",
];
React は内部的にreconcileChildrenArray()
を使用してこれを調整します。
current
のmemoizedProps
は次のようになります:
[
{ $$typeof: Symbol(react.element), type: "button" },
" (",
{ $$typeof: Symbol(react.element), type: "span" },
")",
];
2.10 reconcileChildrenArray()
が必要に応じて fiber を作成・削除
reconcileChildrenArray()
はやや複雑です。
要素の並び替えがあるかどうかをチェックし、key
が存在する場合は fiber の再利用を試みる追加の最適化を行います。
key
については、別途解説記事 How does 'key' work internally? List diffing in React を参照してください。
今回のデモではkey
を使用していないため、基本のブランチ処理に進みます。
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array,
lanes: Lanes,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 既存のfiberと新しい子要素を比較
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
// リスト内の各fiberを新しいpropsでチェック
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
// TODO: null子要素などの空スロットで中断される問題があります。
// 常に低速パスをトリガーするため、残念です。
// ミスかnull、undefined、falseのいずれかであると伝えるより良い方法が必要です。
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// fiberを再利用できない場合、Deletionとしてマーク
// commitフェーズでDOMノードが削除される
// スロットが一致したが、既存のfiberを再利用しなかった場合なので、既存のchildを削除
deleteChild(returnFiber, oldFiber);
//-----------
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// ~ この処理はfiberをInsertionとしてマークしようとします
if (previousNewFiber === null) {
// TODO: ループの外に移動。これは最初の実行時のみ発生します
resultingFirstChild = newFiber;
} else {
// TODO: このスロットの正しいインデックスにない場合、siblingの処理を遅延
// 例えば以前にnull値があった場合、各null値に対してこれを遅延させたい
// ただし、前の要素でupdateSlotを呼び出したくない場合もあります
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// 新しいchildrenの終端に到達。残りを削除できます
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
...
return resultingFirstChild;
}
updateSlot()
は基本的に、key
を考慮しながら新しい props で fiber を作成または再利用します。
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を持たない。前のノードが暗黙的にkey付けされていても、
// テキストノードでなくても置換を継続可能
if (key !== null) {
return null;
}
return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
return updateElement(returnFiber, oldFiber, newChild, lanes);
//-------------
} else {
return null;
}
}
...
}
}
return null;
}
function updateElement(
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
current,
element.props.children,
lanes,
element.key,
);
}
if (current !== null) {
if (
current.elementType === elementType ||
// このチェックはfalseパスでのみ実行されるようインライン化
(__DEV__
? isCompatibleFamilyForHotReloading(current, element)
: false) ||
// Lazyタイプは解決後のタイプでreconcileする必要あり
// ホットリロードチェックの後で実行する必要がある
// ホットリロードは再サスペンドせず、prodとは異なるセマンティクスを持つため
// 以下の呼び出しをサスペンドさせることはできない
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === current.type)
) {
// インデックスに基づいて移動
const existing = useFiber(current, element.props);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
return existing;
}
}
// 挿入
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
<div/>
の場合、updateSlot()
は 3 つの子要素の再利用に成功しましたが、3 番目の要素は
current
がspan
であるのに対して必要な要素がb
であったため、b
の fiber が新規に作成され、
span
の fiber はdeleteChild()
によって削除されます。新しく作成されたb
はplaceChild()
によってマークされます。
2.11 placeChild()
と deleteChild()
による Fiber フラグのマーキング
Ok, for the children of <div>
under Component
, we have these 2 functions
that marks fiber nodes.
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// ハイドレーション中、useIdアルゴリズムはchildrenのリスト(配列やイテレータ)に属するfiberを認識する必要があります
newFiber.flags |= Forked;
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// 移動操作としてマーク
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// アイテムは元の位置に留まることが可能
return oldIndex;
}
} else {
// 新規挿入としてマーク
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) {
// 何もしない
return;
}
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
削除対象の fiber は親の配列に一時的に格納されます。これは削除後新しい fiber ツリーに存在しなくなるため、 コミットフェーズで処理される必要があるからです。そのためどこかに保持しておく必要があります。
以上で<div>
の処理は完了です。
次に React はbutton
に移動します。スケジュールされた work がなくても、
props が["click me-", "1"]
から["click me-", "2"]
に変更されているため、
React はupdateHostComponent()
で処理を続行します。
HostText の場合、props は文字列であるため最初の"click me -"
はベイルアウトします。
その後 React はupdateHostText()
でテキストの調整を試みます。
2.12 updateHostText()
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
// ここでは何もしません。これは終端処理です。完了ステップは直後に行われます
return null;
}
この関数も何もしません。なぜなら更新は complete フェーズ(completeWork()
)でマークされるからです。
この仕組みは初期マウントでも説明されています。
2.13 completeWork()
による HostComponent の更新マーキングと DOM ノード作成
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// 注意: 意図的にハイドレーション状態をチェックしていません。現在のツリープロバイダfiberとの比較が
// 同等の速度でエラー発生率が低いためです。理想的にはハイドレーション専用の
// work loopバージョンが必要です
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// 更新ブランチ
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// ~ この処理は初回マウントの場合のブランチです。前回のエピソードで説明しました
...
}
bubbleProperties(workInProgress);
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {
// これは更新の場合のブランチです
const oldText = current.memoizedProps;
// もしalternateが存在する場合、これは更新を意味し
// 更新を行うための副作用をスケジュールする必要があります
updateHostText(current, workInProgress, oldText, newText);
} else {
// これは[前回のエピソード](/docs/TODO)で説明した初期マウントブランチです
...
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
bubbleProperties(workInProgress);
return null;
}
...
}
}
updateHostText = function (
// 注意: これはcompleteフェーズの異なる`updateHostText()`です
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string
) {
// テキストが異なる場合、更新としてマーク。すべての作業はcommitWorkで行われます
if (oldText !== newText) {
markUpdate(workInProgress);
//--------
}
};
updateHostComponent = function (
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container
) {
// alternateが存在する場合、これは更新を意味し
// 更新を行うための副作用をスケジュールする必要があります
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
// mutationモードでは、childrenが変更されてもこのノードに触れないため
// bailoutにはこれで十分です
return;
}
// childrenの更新によって自身が更新された場合、newPropsを持たないため再利用する必要があります
// TODO: propsとchildrenで別々のupdate APIを分割する
// 理想的にはchildrenを特別扱いしない方が良いでしょう
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// TODO: oldPropsがnullになるエラーが発生中。ホストコンポーネントが
// resumeパスを通過している可能性。原因調査が必要。`hidden`属性関連の可能性あり
const updatePayload: null | UpdatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext
);
// TODO: コンポーネントタイプ固有の型指定が必要
workInProgress.updateQueue = (updatePayload: any);
//----------
// 更新はupdateQueueに格納されます
// ~ この仕組みは[Effect Hooks](/docs/TODO)のようなフックでも使用されます
// updatePayloadが変更を示す場合、または新しいrefがある場合、更新としてマーク
// すべての作業はcommitWorkで行われます
if (updatePayload) {
markUpdate(workInProgress);
//----------
}
};
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
// a PlacementAndUpdate.
workInProgress.flags |= Update;
//---------------
// ~ Yep, another flag!
}
これでレンダーフェーズが完了し、以下の更新が確定しました:
b
への Insertionspan
への Deletion- HostText の更新
button
の更新(内部的には空)
特筆すべき点として、prepareUpdate()
はbutton
とその親div
の両方で実行されますが、
div
ではnull
を生成し、button
では[]
を生成します。これは複雑なエッジケース処理であり、
ここでは詳細に立ち入りません。
これらの更新をコミットフェーズで適用する時が来ました。
3. コミットフェーズでの再レンダー
3.1 commitMutationEffectsOnFiber()
による挿入/削除/更新の開始
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// エフェクトフラグはfiberタイプを特定した*後*にチェックする必要があります
// fiberタグがより具体的なためです。例外はreconciliation関連のフラグで、
// これらはすべてのfiberタイプに設定可能です
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
//----------------------------------
// ~ 最初にchildrenを再帰的に処理
commitReconciliationEffects(finishedWork);
//----------------------------------
// ~ 次にInsertionを処理
if (flags & Update) {
//--------------------
// 最後にUpdateを処理
try {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(
HookInsertion | HookHasEffect,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
...
}
return;
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
//----------------------------------
// ~ 最初にchildrenを再帰的に処理
commitReconciliationEffects(finishedWork);
//----------------------------------
// ~ 次にInsertionを処理
if (supportsMutation) {
// TODO: ContentResetはコミットフェーズ中にchildrenによってクリアされる
// これはリファクタリングリスク要因です。なぜなら`commitReconciliationEffects`実行後の
// フラグを読み取る必要があるため、順序が重要です。ContentResetがコミット中のフラグ変更に
// 依存しないようリファクタリングすべきです(例:レンダーフェーズでフラグを設定するなど)
if (finishedWork.flags & ContentReset) {
const instance: Instance = finishedWork.stateNode;
try {
resetTextContent(instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
if (flags & Update) {
//--------------------
// 最後にUpdateを処理
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// 事前に準備された作業をコミット
const newProps = finishedWork.memoizedProps;
// ハイドレーションではupdateパスを再利用しますが、oldPropsを
// newPropsとして扱います。この場合、updatePayloadに実際の変更が含まれます
const oldProps =
current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: ホストコンポーネント固有の型指定が必要
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
try {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
}
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
if (supportsMutation) {
if (finishedWork.stateNode === null) {
throw new Error(
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// ハイドレーションではupdateパスを再利用しますが、oldPropsを
// newPropsとして扱います。この場合、updatePayloadに実際の変更が含まれます
const oldText: string =
current !== null ? current.memoizedProps : newText;
try {
commitTextUpdate(textInstance, oldText, newText);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
...
}
}
この処理が再帰的なプロセスであることがわかります。それぞれの変更の種類を詳しく見ていきましょう。
3.2 削除処理は、子要素と自身の処理前、最初に実行
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes
) {
// Deletionsエフェクトは任意のfiberタイプでスケジュール可能です。これらは
// childrenエフェクトが発火する前に処理される必要があります
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
commitDeletionEffects(root, parentFiber, childToDelete);
//---------------------
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitMutationEffectsOnFiber(child, root, lanes);
//----------------------------
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
Deletions are processed first, even before processing children.
function commitDeletionEffects(
root: FiberRoot,
returnFiber: Fiber,
deletedFiber: Fiber
) {
if (supportsMutation) {
// 削除対象の最上位Fiberのみ保持していますが、すべての末端ノードを見つけるため
// childrenを再帰的に探索する必要があります
// 親からすべてのhost nodesを再帰的に削除し、refsをデタッチし、
// マウントされたレイアウトエフェクトをクリーンアップし、componentWillUnmountを呼び出します
// 各ブランチで最上位のhost childのみ削除すれば十分ですが、エフェクト、refs、
// cWU(componentWillUnmount)のアンマウントのために走査を続ける必要があります。
// TODO: これを2つの別々の走査関数に分割可能です(2つ目はremoveChildロジックを含まない)。
// これは「disappearLayoutEffects」と同じ機能になる可能性があります(レイアウトフェーズが
// 再帰を使用するようにリファクタリングされた後の名称に関わらず)
// 開始前に、スタック上の最も近いhost parentを見つけます
// これによりchildrenを削除する対象のインスタンス/コンテナがわかります
// TODO: 削除毎にfiber returnパスを検索する代わりに、コミットフェーズでツリーを
// 走査しながらJSスタック上で最も近いhost componentを追跡できます
// これにより挿入処理も高速化されます
let parent = returnFiber;
findParent: while (parent !== null) {
//--------------------------------
// 親ノードが必ずしもバッキングDOMを持つとは限らないため
// ここではバッキングDOMを持つ最も近いFiber Nodeを検索します
switch (parent.tag) {
case HostComponent: {
hostParent = parent.stateNode;
hostParentIsContainer = false;
break findParent;
}
case HostRoot: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
case HostPortal: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
}
parent = parent.return;
}
if (hostParent === null) {
throw new Error(
"Expected to find a host parent. This error is likely caused by " +
"a bug in React. Please file an issue."
);
}
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
//----------------------------
hostParent = null;
hostParentIsContainer = false;
} else {
// refsをデタッチし、サブツリー全体でcomponentWillUnmount()を呼び出します
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
//----------------------------
}
detachFiberMutation(deletedFiber);
}
3.3 次に挿入処理が実行
新しく作成されたノードがツリー構造でセットアップできるようにするためです。
function commitReconciliationEffects(finishedWork: Fiber) {
// Placementエフェクト(insertions、reorders)は任意のfiberタイプでスケジュール可能です
// これらはchildrenエフェクト発火後、かつ当該fiberのエフェクト発火前に処理される必要があります
const flags = finishedWork.flags;
if (flags & Placement) {
try {
commitPlacement(finishedWork);
//---------------
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// componentDidMountのようなライフサイクルが呼ばれる前に
// 挿入済みであることを示すため"placement"フラグをクリア
// TODO: findDOMNodeはこれに依存しなくなったがisMountedは依然使用
// ただしisMountedは非推奨のため削除可能
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// 再帰的に全てのホストノードを親要素に挿入
const parentFiber = getHostParentFiber(finishedWork);
// Node: これら2つの変数は常に一緒に更新する必要があります
switch (parentFiber.tag) {
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
// 挿入前に親要素のテキストコンテンツをリセット
resetTextContent(parent);
// ContentResetフラグをクリア
parentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// ~ ここは重要です。[Node.insertBefore()](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore)は兄弟ノードが必要です
// ~ もし兄弟ノードが見つからない場合、最後に追加されます
// 挿入された一番上のFiberのみを持っていますが、その子要素を再帰的に処理して
// 全ての終端ノードを見つける必要があります
insertOrAppendPlacementNode(finishedWork, before, parent);
//---------------------------
break;
}
case HostRoot:
case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
// eslint-disable-next-line-no-fallthrough
default:
throw new Error(
"Invalid host parent fiber. This error is likely caused by a bug " +
"in React. Please file an issue."
);
}
}
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container
): void {
const { tag } = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode: Instance = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
// HostPortal自体が挿入対象の場合、子要素を再帰処理せず
// 各子要素から直接挿入処理を受け取ります
// HostSingletonの場合も独立して配置されます
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
function insertOrAppendPlacementNode(
node: Fiber,
before: ?Instance,
parent: Instance
): void {
const { tag } = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode: Instance = node.stateNode;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
} else if (tag === HostPortal) {
// HostPortal自体が挿入対象の場合、子要素を再帰処理せず
// 各子要素から直接挿入処理を受け取ります
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
3.4 更新処理は最後に処理
Update branch is inside commitMutationEffectsOnFiber()
.
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// エフェクトフラグはfiberタイプを特定した*後*にチェックする必要があります
// fiberタグがより具体的なためです。例外はreconciliation関連のフラグで、
// これらはすべてのfiberタイプに設定可能です
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
//For FunctionComponent, this means hooks need to be run
try {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(
HookInsertion | HookHasEffect,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
if (supportsMutation) {
// TODO: ContentReset gets cleared by the children during the commit
// phase. This is a refactor hazard because it means we must read
// flags the flags after `commitReconciliationEffects` has already run;
// the order matters. We should refactor so that ContentReset does not
// rely on mutating the flag during commit. Like by setting a flag
// during the render phase instead.
if (finishedWork.flags & ContentReset) {
const instance: Instance = finishedWork.stateNode;
try {
resetTextContent(instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
if (flags & Update) {
//For HostComponent, this means element attributes are needed to be updated
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps =
current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
try {
commitUpdate(
//------------
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
}
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
if (supportsMutation) {
if (finishedWork.stateNode === null) {
throw new Error(
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string =
current !== null ? current.memoizedProps : newText;
try {
commitTextUpdate(textInstance, oldText, newText);
//----------------
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
...
}
}
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object
): void {
// Apply the diff to the DOM node.
updateProperties(domElement, updatePayload, type, oldProps, newProps);
//----------------
// Update the props handle so that we know which props are the ones with
// with current event handlers.
updateFiberProps(domElement, newProps);
}
export function commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string
): void {
textInstance.nodeValue = newText;
}
今回のデモでは、ツリー構造のため、以下の順序で mutation が処理されます:
span
の Deletion- HostText の Update
button
の Update(内部的には空の更新)b
の Insertion
4. まとめ
かなりの情報量でしたね。再レンダープロセスを大まかにまとめると以下のようになります:
- ステート変更後、対象の Fiber Node へのパスに
lanes
とchildLanes
がマークされ、 自身またはサブツリーの再レンダーが必要かどうかを示します - React は不要な再レンダーを避けるための bailout を最適化しつつ、 ファイバーツリー全体を再レンダーします
- コンポーネントが再レンダーされると新しい React 要素を生成し、子要素は等価でも
新しい props を受け取るため、デフォルトでファイバーツリー全体が再レンダーされます
これが
useMemo()
が必要となる理由です - 「再レンダー」とは、現在のツリーから新しいファイバーツリーを作成し、必要に応じて
Fiber Node に
Placement
ChildDeletion
Update
フラグを設定するプロセスです - 新しいファイバーツリーが完成すると、React は上記フラグを持つ Fiber Node を処理し、 コミットフェーズで Host DOM に変更を適用します
- 新しいファイバーツリーが現在のツリーとして設定され、前のツリーのノードは 次回のレンダーで再利用される可能性があります
React の内部動作についてさらに知りたいですか? React Internals Deep Diveシリーズをチェックしてください!