🙌

React差分検出処理の内部実装①

2022/09/02に公開

背景

今までReactの内部動きについて、以下の三つを紹介しました。

最初のマウントのFiberツリーの動作
https://zenn.dev/villa_ak99/articles/2e4194e8367452

最初マウントから一回目の更新までのFiberツリーの動作
https://zenn.dev/villa_ak99/articles/af47a6601c877c

一回目の更新完了から二回目更新までのFiberツリーの動作
https://zenn.dev/villa_ak99/articles/f5bd881bf3e34a

これらの動作は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を使い続くための条件としては以下になります

  1. keyは同じ
  2. 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を作ります。

※ 記事の書きが汚くすみません、内容は自己研鑽で間違いないです、縁があれば交流しましょう。

Discussion