Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React inside basics: learn from “build own react"

React inside basics: learn from “build own react"

Avatar for ryounasso

ryounasso

June 21, 2025
Tweet

More Decks by ryounasso

Other Decks in Programming

Transcript

  1. Build own React というブログ記事の内容に取り組んでみました。 この記事では実際の React のアーキテクチャに沿って、React を 1 から書いていきま

    す。 最適化機能を実装していなかったり、完全に再現しているわけではありません。 取り組む中で React の内部実装の基礎知識を得ることができたので、その内容につい てお話しします。 関数型コンポーネントを前提としています。 2
  2. 1. 仮想 DOM React の基本的な考え方 → 状態が変わるごとにコンポーネントを毎回実行して DOM を新規に構築 ただ、実

    DOM を操作すると、パフォーマンス面が厳しい。 → そこで 仮想 DOM 仮想 DOM は JavaScript オブジェクトの木構造 実際は仮想 DOM という単語は使用しないとのこと (便宜上今回は仮想 DOM と呼ぶ) 以下の記事で詳しく解説されている 「仮想DOM」という用語を使わない 5
  3. 仮想 DOM の例 { type: "div", props: { children: [

    { type: "p", props: { children: ["Count: ", state] } }, { type: "button", props: { onClick: () => setState((c) => c + 1), children: "Count up" } } ] } } 6
  4. const container = document.getElementById("root") // element: 仮想 DOM オブジェクト Didact.render(element,

    container) function render(element, container) { // element.type に合わせて実 DOM 要素を生成 const dom = element.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(element.type) const isProperty = key => key !== "children" Object.keys(element.props) .filter(isProperty) .forEach(name => { dom[name] = element.props[name] }) element.props.children.forEach(child => render(child, dom) ) // 生成した DOM を追加 container.appendChild(dom) } 仮想 DOM オブジェクトの内容に沿って実 DOM を生成してレンダリングする 7
  5. 2. 作業単位の分割とスケジューリング 大きなコンポーネントだとツリーが大きくなる → コンポーネントのレンダリング中スレッドを占有してしまう 解決策: レンダリング作業を小さな単位に分割 → Fiber 特徴:

    各Fiberは1つの作業単位(要素)を表現 親・子・兄弟の参照を持つ連結リスト alternate プロパティで前回のレンダリング結果を保持 処理の分割により Fiber のスケジューリングが可能 優先度の低い Fiber はブラウザのアイドル時間に処理 優先度の高い Fiber は早いタイミングで実行 8
  6. { type: "div", props: { children: [pFiber, buttonFiber] }, dom:

    div, // 実際の DOM 要素への参照 parent: counterFiber, // 親 Fiber child: pFiber, // 第一の子要素の Fiber sibling: null, // 同じ親を持つ要素の Fiber alternate: null, // 前回のレンダリング結果を保持する Fiber effectTag: "UPDATE" // DOM 操作のタイプ } // button Fiber { type: "button", props: { onClick: () => setState((c) => c + 1), children: ["Count up"] }, dom: button, // 実際の DOM 要素への参照 parent: divFiber, child: buttonTextFiber, // ボタンテキストの Fiber sibling: null, alternate: oldButtonFiber, effectTag: "UPDATE" } 10
  7. 3. レンダーとコミットの分離 Fiber によって優先度の高い処理を優先的に行うことが可能になった 例:ユーザーの入力処理の割り込みが入る レンダリング途中で DOM を更新すると不完全な UI が表示される

    →「レンダー」と「コミット」の2フェーズに分離 レンダーフェーズ: 仮想 DOM の差分計算を行う(中断可能) DOM 操作は一切行わない コミットフェーズ: 差分計算完了後に実行(中断不可) まとめて DOM 操作を実行 11
  8. 登場するグローバル変数の説明 nextUnitOfWork : 次に処理すべき Fiber wipRoot : 作業中の Fiber のルート

    currentRoot : 前回コミットされた Fiber ツリー deletions : 削除すべき Fiber のリスト 12
  9. レンダリングフェーズ function workLoop(deadline) { // shouldYield: ブラウザにレンダリング制御を戻すべきかを示すフラグ while (nextUnitOfWork &&

    !shouldYield) { // performUnitOfWork: 1つの Fiber の処理と次の Fiberの取得 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 残された時間が1ms未満になったら中断。高優先度タスク(入力処理やアニメーション)に道を譲る shouldYield = deadline.timeRemaining() < 1; } // === レンダーフェーズ完了時の処理 === if (!nextUnitOfWork && wipRoot) { // レンダーフェーズで計算した変更を実 DOM に適用 commitRoot(); } } 13
  10. コミットフェーズ function commitRoot() { // deletions: 削除すべきノードのリスト // 削除操作を最初に行う deletions.forEach(commitWork);

    // wipRoot.child: ルートファイバーの子から始めて再帰的に全ての変更をコミット commitWork(wipRoot.child); // 次回の差分計算のために現在のルートを完了したルートで更新 currentRoot = wipRoot; // currentRoot: 前回コミットされたファイバーツリー // 作業用ルートをクリア(コミット完了の印) wipRoot = null; } 14
  11. コミット作業を行う。すべてのノードを DOM に再帰的に追加する。 function commitWork(fiber) { if (!fiber) { return;

    } let domParentFiber = fiber.parent; while (!domParentFiber.dom) { domParentFiber = domParentFiber.parent; } const domParent = domParentFiber.dom; if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { domParent.appendChild(fiber.dom); } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props); } else if (fiber.effectTag === "DELETION") { commitDeletion(fiber, domParent); } commitWork(fiber.child); commitWork(fiber.sibling); } 15
  12. 4. Reconciliation DOM にレンダリングしたい内容と、前回のレンダリングの内容を比較して差分を検知 する 比較した結果、DOM に適用すべき変更があるかどうかを確認する 適用すべき変更内容の確認方法 古い Fiber

    と新しい Fiber の要素が同じタイプ = 更新 古い Fiber と新しい Fiber のタイプが異なり、新しい要素がある = 新規追加 古い Fiber と新しい Fiber のタイプが異なり、古い Fiber がある = 削除 16
  13. Reconciliation の処理 // elements: 新しくレンダリングしたい要素の配列 function reconcileChildren(wipFiber, elements) { let

    oldFiber = wipFiber.alternate?.child; // 新しい Fiber と今の Fiber を比較する for (let i = 0; i < elements.length || oldFiber != null; i++) { const element = elements[i]; const sameType = oldFiber && element && element.type == oldFiber.type; if (sameType) { // 更新: 同じ型の場合はDOMノードを保持し、プロパティのみ更新 // effectTag: "UPDATE" } else if (element) { // 新規作成: 新しい要素がある場合 // effectTag: "PLACEMENT" } else if (oldFiber) { // 削除: 古い要素のみ存在する場合 // effectTag: "DELETION" } } } 17
  14. レンダーフェーズ reconcileChildren 関数が実行され、同じ p タグ同士であるため、 sameType が true となる 後のコミットフェーズでの処理のために

    p の Fiber の alternate や effectTag を 更新する if (sameType) { // update the node newFiber = { // ... alternate: oldFiber, effectTag: "UPDATE", }; } 19
  15. コミットフェーズ commitRoot 関数が実行され、 p の effectTag が "UPDATE" なので updateDom

    関 数が実行される if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props); // テキストノードの内容が"カウント: 2"に更新される } これにより、変更内容が DOM に反映されてユーザーにはカウンターの値が 2 と表示さ れる 20
  16. 5. hooks React が提供する API 状態を管理 useState , useReducer 作用を管理

    useEffect , useLayoutEffect メモ化 useMemo , useCallback 参考 React https://speakerdeck.com/recruitengineers/react-2023?slide=108 21
  17. useState 状態を扱うためのフック 特徴 関数コンポーネントで状態を維持することが可能になる 各コンポーネントの Fiber に配列として保存される 初期値を引数に取り、現在の状態と状態更新関数を返す setState を呼ぶと、コンポーネントの再レンダリングを発火

    状態更新はすぐには適用されず、次のレンダリングサイクルで処理される 複数の useState を使って、複数の状態を管理できる 呼び出し順序に基づいてフックを識別(インデックスを使用) 22
  18. function useState(initial) { // 前回のレンダリング時のフックを取得する。 wipFiber.alternate.hooks に前回のフックが保存されている const oldHook =

    wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // フックのオブジェクトを生成。前回のフックが存在すれば前回の状態、なければ初期値を使用。queue は状態更新関数を保存する配列 const hook = { state: oldHook ? oldHook.state : initial, queue: [] }; // 前回のレンダリングから適用されていない状態更新関数を適用することで常に最新の state が返却される const actions = oldHook ? oldHook.queue : []; actions.forEach((action) => { hook.state = action(hook.state); }); // 状態を更新するための関数 const setState = (action) => { // 省略 }; // 現在の Fiber にこのフックの情報を保存。複数の state を管理するために Fiber には配列でフックを保存 wipFiber.hooks.push(hook); // 次のフックのためにインデックスを増やす。 hookIndex++; return [hook.state, setState]; } 23
  19. setState 関数の実装 // 状態を更新するための関数 const setState = (action) => {

    // 更新アクションをキューに追加。これで次回のレンダリング時に oldHook.queue から取得可能 hook.queue.push(action); // 新しい Fiber のルートを作成 wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, // 現在のツリーを前回のツリーとして参照 }; // nextUnitOfWork が次に処理すべき Fiber を格納する変数 // 新しいレンダリングフェーズを開始するために新しい wipRoot を次の作業単位として設定。 // この処理があるから、setState の呼び出しで再レンダリングが発火する nextUnitOfWork = wipRoot; deletions = []; }; 24
  20. useEffect とは? 関数コンポーネントで「副作用」を扱うためのフック 副作用: レンダリング以外の処理(データ取得、購読、DOMの直接操作など) 依存配列とエフェクト関数、クリーンアップ関数を持つ useEffect(() => { document.title

    = `クリック数: ${count}`; // クリーンアップ関数(任意) return () => { console.log('コンポーネントがアンマウントされるか再レンダリングされます'); }; }, [count]); // 依存配列 27
  21. useEffect の実装において考えること フックの情報管理 フックを Fiber に保存して再レンダリング間で状態を維持 依存配列、エフェクト関数、クリーンアップ関数、を保持 依存配列の比較 依存配列に変更があった場合のみエフェクトを実行 初回レンダリング時は無条件で実行

    エフェクトの実行タイミングの管理 DOM更新(コミットフェーズ)完了後にエフェクトを実行 副作用なので、DOM が更新された後に実行する必要がある 再レンダリング時とアンマウント時にクリーンアップ関数を実行 28
  22. フックの情報管理 // 副作用(エフェクト)をコンポーネントのレンダリング後(コミットフェーズ後)に実行するための準備をする関数 function useEffect(effect, deps) { // 現在の Fiber

    から前回のフックを取得 const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // 保存したいフック情報を作成 const hook = { effect, // エフェクト関数 deps, // 依存配列 cleanup: oldHook ? oldHook.cleanup : undefined, // クリーンアップ関数。前回の effect で返されたクリーンアップ関数を実行する必要がある fiber: wipFiber // Fiberへの参照 }; // Fiber にフック情報を保存 wipFiber.hooks.push(hook); hookIndex++; // エフェクトの実行自体は後のコミット完了後に行うため、ここではフックを保存するだけ effectHooks.push(hook); } フックを Fiber に保存して再レンダリング間で状態を維持 依存配列、エフェクト関数、クリーンアップ関数を保持 29
  23. 依存配列の比較 function areHookDepsEqual(prevDeps, nextDeps) { if (!prevDeps || !nextDeps) return

    false; if (prevDeps.length !== nextDeps.length) return false; return prevDeps.every((dep, i) => Object.is(dep, nextDeps[i])); } 30
  24. function useEffect(effect, deps) { const oldHook = wipFiber && wipFiber.alternate

    && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; const hook = { effect, deps, cleanup: oldHook ? oldHook.cleanup : undefined, fiber: wipFiber // fiberへの参照を追加 }; // 依存配列が変更されたか、または初回実行時はエフェクトを実行キューに追加 const hasChangedDeps = oldHook ? !areHookDepsEqual(oldHook.deps, hook.deps) : true; if (hasChangedDeps) { // エフェクトの実行自体は後のコミット完了後に行うため、ここではフックを保存するだけ。`runEffects` 関数で実行される effectHooks.push(hook); } wipFiber.hooks.push(hook); hookIndex++; } 31
  25. 実行タイミングの管理 DOM更新後にエフェクトを実行 → DOMが更新されてから副作用を実行しないと、古い状態に対して操作してしまう // コミットフェーズで呼び出す関数 function commitRoot() { //

    DOM への反映処理 deletions.forEach(commitWork); commitWork(wipRoot.child); // DOM更新後にエフェクトを実行 runEffects(); // 次のレンダリングサイクルの準備 currentRoot = wipRoot; wipRoot = null; } 32
  26. エフェクトの実行と再レンダリング時のクリーンアップ処理 function runEffects() { // effectHooks をコンポーネント単位でグループ化して処理 const componentEffects =

    {}; // コンポーネントごとにエフェクトをグループ化・コンポーネントIDをキーとして使用 effectHooks.forEach((hook) => { const componentId = hook.fiber.type.name || "undefined"; if (!componentEffects[componentId]) { componentEffects[componentId] = []; } componentEffects[componentId].push(hook); }); // 各コンポーネント内で前のエフェクトのクリーンアップを実行してから、新しいエフェクトを実行 Object.values(componentEffects).forEach((hooks) => { hooks.forEach((hook) => { if (hook.cleanup) { hook.cleanup(); } }); hooks.forEach((hook) => { hook.cleanup = hook.effect(); }); }); effectHooks = []; } 33
  27. アンマウント時のクリーンアップ関数の実行 // コンポーネントがアンマウントされる際に呼び出す関数 function commitDeletion(fiber, domParent) { // コンポーネント削除前にクリーンアップ関数を実行 if

    (fiber.hooks) { fiber.hooks.forEach(hook => { if (hook.cleanup) { hook.cleanup(); } }); } if (fiber.dom) { domParent.removeChild(fiber.dom); } else { commitDeletion(fiber.child, domParent); } } 34
  28. 実装した useEffect を使う function useEffect(effect, deps) { // 省略 }

    export const Didact = { createElement, render, useState, useEffect }; Didact.useEffect(() => { document.title = `クリック数: ${count}`; return () => { console.log("Counter unmounted or updated"); }; }, [count]); 35
  29. まとめ React の内部実装の基礎知識を学ぶことができた 仮想 DOM の概念 作業単位の分割とスケジューリング (Fiber) レンダーとコミットの分離 Reconciliation

    の仕組み hooks の実装方法 これによって今後のアップデートの意図や仕様の理解がスムーズになる予感 36