🙌
React差分検出処理の内部実装①
背景
今までReactの内部動きについて、以下の三つを紹介しました。
最初のマウントのFiberツリーの動作
最初マウントから一回目の更新までのFiberツリーの動作
一回目の更新完了から二回目更新までのFiberツリーの動作
これらの動作はFiberツリーにあるFiberNodeの種類は変わることがない例でした。
これらの例は差分検出処理を通していますが、あまり顕著に見えないですので。
今回は更新によりFiberNodeが変わる例を出して、差分検出処理の一つを説明いたします。
Reactバージョン情報
"react": "^18.0.2",
"react-dom": "^18.0.2",
コード
import { useState } from 'react';
function App() {
const [num,setNum] = useState(0)
if (num % 2 === 0) {
return (
<div className="container" onClick={() => setNum(num + 1)}>
<div className='subContainer'>
even
</div>
</div>
);
}
return (
<div className="container" onClick={() => setNum(num + 1)}>
<p className='subContainer'>
odd
</p>
</div>
);
}
export default App;
これらのコードはnumを6になっていることを事前に操作しています。なぜかというと、numが6になっていあるなら、もう6回レンダリングが終わって、メモリ上ににきちんと二つの飽和的なFiberツリーが存在していって、差分検出の特徴を見やすいです。
レンダリングフェーズ
初期状態
- fiberRootNodeのcurrentプロパティーは左側のFiberツリーに指しています。
- 左側のFiberツリーはnum=6の時に構築したFiberツリーです。
- 右側のFiberツリーはnum=5の時に構築したFiberツリーです。
クリックして、setNumを実行
- reactDomのdispatchSetStateを実行します。setNumは実はjavascriptのclosureを生かしたdispatchSetState関数になり、第一引数はApp関数のFiberNodeに指しています。
- dispatchSetState関数の中、currentのFiberツリーのApp関数のFiberNodeのmemorizedStateというプロパティーにアップデート情報を入れます。
- 再レンダリングを促します。
再レンダリング(Top -> Down)
workInProgress(構築中)FiberツリーのrootFiberのchildを調整します
- workInProgressは右側のFiberツリー(前々回のレンダリングに構築したFiberツリーを元にこれから構築するFiberツリー)のトップであるrootFiberに移動します。
- currentは左側のFiberツリー(前回のレンダリング構築したFiberツリー)のトップであるrootFiberに移動します。
- workInProgressのchildを調整します。前々回のレンダリングに構築下App関数のFiberNodeをそのまま使います。(差分検出処理)
- そのまま使い回しますが、プロパティーを調整し直します。その中、特徴がある二つのプロパテーは以下の二つです。
- memorizedStateをcurrentツリーのApp関数FiberNodeのmemorizedStateに指します。
- childをcurrentツリーのApp関数FiberNodeのchildに指します。
App関数のFiberNodeにたどり着き,childを調整します
- App関数を実行します
- App関数FiberNodeのhookを消化します。
- JSXからReact.Elementオブジェクトを得られます。
以下のReact.Elementを得ています。それはあとFiberツリーを構築する材料となります。
省略あり。
$$typeof: Symbol(react.element)
key: null <- これから差分検出するに使うkeyです。
type: "div" <- これから差分検出するに使うtypeです。
props:
className: "container"
onClick: () => setNum(num + 1)
children:
$$typeof: Symbol(react.element)
type: "p"
key: null
props:
className: "subContainer"
children: "odd"
- ここで差分検出が行います、左側のdiv.containerFiberNodeのkeyとtypeを上記のReact.Elementオブジェクトのkeyとtypeを比較します。
同じであれば、右側のdiv.containerFiberNodeをそのまま使い続きます。
同じでなければ、上記のReact.Elementオブジェクトを元に新しいFiberNodeオブジェクトを作ります。
ここはkeyはtypeとも同じなので、右側のdiv.containerFiberNodeをそのまま使い続きます。 - 使い続くFiberNodeのプロパテーを更新します。
- childを左側のdiv.containerFiberNodeのchildに指します。
- pendingPropsを計算したReact.Elementのpropsに指します。
- そのほかのmemorizedState,memorizedPropsは左側のdiv.containerFiberNodeのプロパテーに指します。
div.containerのFiberNodeにたどり着き,childを調整します
- 差分検出処理:以下の材料(React.Elementオブジェクト)を使って、左側のdiv.subContainerのFiberNodeと比較します。
$$typeof: Symbol(react.element)
type: "p" <- これから差分検出するに使うtypeです。
key: null <- これから差分検出するに使うkeyです。
props:
className: "subContainer"
children: "odd"
- 左側のdiv.subContainerのFiberNodeのkeyとtype情報は以下になります
key: null
type: div
左側のdiv.subContainerのFiberNodeを使い続くための条件としては以下になります
- keyは同じ
- typeは同じ
なので、keyは両方ともnullですが、typeはそれぞれ"p"と"div"になります。
そのため、左側のdiv.subContainerのFiberNodeを使い続くことができません。
FiberNodeのオブジェクトを作ることになります。
FiberNodeオブジェクトは上記なReact.Elementをもとに作ります。
- 作ったFiberNodeオブジェクトはworkInProgressのchildとして調整します。
- 左側のdiv.subContainerを削除予定にします。左側のdiv.subContainerのFiberNodeをworkInProgressのdeletionsプロパテーに入れます。あとのcommitフェーズでそれをもとに、画面上に残るevenを削除します。
p.containerのFiberNodeにたどり着き,childを調整します
- childは特にはないですので、Top -> Downのdfsはここまでになります。
再レンダリング(bottom -> top)
p.containerのFiberNodeからバックトラッキング、stateNodeを作ります。
- p.containerのFiberNodeは新しく作成したFiberNode、該当するdom要素はないから、作成して、stateNodeプロパティーに入れます。
新しく作成したstateNodeは以下のようになります。
<p class="subContainer">odd</p>
div.containerのFiberNodeに戻ります
- updateQueueを計算した結果[]になり、updateQueueを更新します。
App関数のFiberNodeに戻ります
- 特になにもしていません
rootFiberに戻ります
- 特になにもしていません
ここまでレンダリングフェーズが終わります。
特に以下のことをしました。
- 構築のFiberツリーにdiv.containerのFiberNodeのchildを新しいFiberNodeにしました。
- 新しく作られたp.subContainerのFiberNodeのstateNodeに新たに作成dom要素をいれました。
- div.containerのFiberNodeは既存のFiberNodeを使い続きます。
- 使い続けたdiv.containerのFiberNodeのdeletionsプロパティーにdiv.subContainerのFiberNodeを入れます。commitフェーズ時に削除するに使います。
commitフェーズ
右側構築中のFiberツリーのTop->Downまで再帰でDFSします。
rootFiberから始まります
- 特になにもしていません。
App関数のFiberNodeにたどり着きます
- 特に何もしていません。
div.containerのFiberNodeにたどり着きます
- FiberNodeにdeletionsプロパティーにcurrentツリーのdiv.subContainerFiberNodeを保存しています。
- それはcurrentツリーのdiv.subContainerFiberNode.stateNodeが指していあるdom要素を削除する意味です。
- currentツリーのdiv.subContainerFiberNode.stateNodeから指していあるdom要素は以下になります。
<div class="subContainer">even</div>
- 削除したdomに残ってあるのは以下になります。
<div class="container"></div>
- 削除する経緯は以下になります。
p.subContainerのFiberNodeにたどり着きます
- このFiberNodeに以前レンダリングのバックトラッキング段階に、placementというフラグを付けましたので、このFiberNodeはdomに追加するものがあります。
- このFiberNodeのstateNodeをこのFiberNodeの親のstateNodeにappendします。
appendする経緯はいかになります。
div.containerのFiberNodeに戻ります
- 特に大きなやることはありません。
App関数のFiberNodeに戻ります
- 特に大きなやることはありません。
rootFiberに戻ります
- 特に大きなやることはありません。
ここまではコミットフェーズのcommitMutationEffectsが終わります。
currentツリーを右側のツリーに指します
- fiberRootNodeのcurrentプロパティーを右側のFiberツリーに指すようにします。
まとめ
- 差分検出処理はFiberNodeとReact.Elementを比較する処理です
- React.ElementはApp関数を実行の戻り値です。(React17前はbabelでコンパイルされ、React17以降はReact runtime?からコンパイルされています)
- まずはFiberNodeのkeyとReact.Elementのkeyを比較します
- 同じなら、FiberNodeのtypeとReact.Elementのtypeと比較します
- 同じなら、前々回のレンダリングのFiberNodeを使い続きます
- 同じではないなら、React.Elementから新しいFiberNodeを作ります。
- 同じではないなら、React.Elementから新しいFiberNodeを作ります。
- 同じなら、FiberNodeのtypeとReact.Elementのtypeと比較します
※ 記事の書きが汚くすみません、内容は自己研鑽で間違いないです、縁があれば交流しましょう。
Discussion