Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
React inside basics: learn from “build own react"
Search
ryounasso
June 21, 2025
Programming
240
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
React inside basics: learn from “build own react"
ryounasso
June 21, 2025
More Decks by ryounasso
See All by ryounasso
明日から始めるリファクタリング
ryounasso
0
290
駆け足で Google から学ぶテスト設計の指針
ryounasso
0
230
抽象データ型について学んだ
ryounasso
0
470
開発効率向上のためのリファクタリングの一歩目の選択肢 ~コード分割~ / JJUG CCC 2024 Fall
ryounasso
0
4.3k
Clean Architecture by TypeScript & NestJS
ryounasso
0
1.2k
Fast API を用いた Web API の開発
ryounasso
1
640
テストゼロの個人開発プロジェクトにテストを導入した話
ryounasso
0
510
簡易 DI コンテナを作って DI コンテナを知る
ryounasso
1
1.4k
TypeScript_コンパイラの内側に片足を入れる
ryounasso
3
1.1k
Other Decks in Programming
See All in Programming
XP祭りでしか伝わらないフリップネタ #xpjug
murabayashi
0
110
Kiroで創り、AgentCoreで繋ぐ!AWSで実践する「AI-DLC」から「AIエージェント統合」までの最新地図
licux
2
350
LoopHub - ローカルで動く GitHub で、AI と共同開発
jugyo
0
450
Claude Codeを組織的に動かして月400PRを実現した話
happy_ryo
0
260
Go × SIMDで高速化するベクトル検索 ~ルーフラインモデルでSIMDが効く境界を探れ! ~
po3rin
1
140
Vibes Containers 〜AIで変わるコンテナ設計と運用〜
tkikuc
2
500
Swift愛好会と私(ウホーイ) / Swift Fan Club and Uhooi
uhooi
0
140
XHTMLが残したもの
yosuke_furukawa
PRO
2
430
20260828_品質と開発生産性を両立させる、AI時代のE2Eテストの考え方
magicpod
0
160
書籍「プロフェッショナルAI駆動開発」紹介スライド
juntaromatsumoto
0
1k
Go を使い始めて 2 ヶ月の学び / My first two months with Go
contour_gara
0
420
コンパウンドプロダクト開発のためのローカルプロセスマネージャー再発明 #layerxgo
izumin5210
0
660
Featured
See All Featured
Product Roadmaps are Hard
iamctodd
55
13k
Intergalactic Javascript Robots from Outer Space
tanoku
273
27k
Embracing the Ebb and Flow
colly
88
5.2k
Designing Experiences People Love
moore
143
24k
Bootstrapping a Software Product
garrettdimon
PRO
306
120k
Agile that works and the tools we love
rasmusluckow
331
22k
Exploring the Power of Turbo Streams & Action Cable | RailsConf2023
kevinliebholz
37
6.6k
The agentic SEO stack - context over prompts
schlessera
0
900
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
11
1k
jQuery: Nuts, Bolts and Bling
dougneiner
66
8.6k
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.9k
How to make the Groovebox
asonas
2
2.4k
Transcript
React inside basics: learn from “build own react” 1
Build own React というブログ記事の内容に取り組んでみました。 この記事では実際の React のアーキテクチャに沿って、React を 1 から書いていきま
す。 最適化機能を実装していなかったり、完全に再現しているわけではありません。 取り組む中で React の内部実装の基礎知識を得ることができたので、その内容につい てお話しします。 関数型コンポーネントを前提としています。 2
目次 React の内部の基礎知識 i. 仮想 DOM ii. 作業単位の分割とスケジューリング iii. レンダーとコミットの分離
iv. Reconciliation v. hooks 簡易的な useEffect を実装してみる 3
React の内部の基礎知識 4
1. 仮想 DOM React の基本的な考え方 → 状態が変わるごとにコンポーネントを毎回実行して DOM を新規に構築 ただ、実
DOM を操作すると、パフォーマンス面が厳しい。 → そこで 仮想 DOM 仮想 DOM は JavaScript オブジェクトの木構造 実際は仮想 DOM という単語は使用しないとのこと (便宜上今回は仮想 DOM と呼ぶ) 以下の記事で詳しく解説されている 「仮想DOM」という用語を使わない 5
仮想 DOM の例 { type: "div", props: { children: [
{ type: "p", props: { children: ["Count: ", state] } }, { type: "button", props: { onClick: () => setState((c) => c + 1), children: "Count up" } } ] } } 6
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
2. 作業単位の分割とスケジューリング 大きなコンポーネントだとツリーが大きくなる → コンポーネントのレンダリング中スレッドを占有してしまう 解決策: レンダリング作業を小さな単位に分割 → Fiber 特徴:
各Fiberは1つの作業単位(要素)を表現 親・子・兄弟の参照を持つ連結リスト alternate プロパティで前回のレンダリング結果を保持 処理の分割により Fiber のスケジューリングが可能 優先度の低い Fiber はブラウザのアイドル時間に処理 優先度の高い Fiber は早いタイミングで実行 8
https://pomb.us/build-your-own-react/ 9
{ 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
3. レンダーとコミットの分離 Fiber によって優先度の高い処理を優先的に行うことが可能になった 例:ユーザーの入力処理の割り込みが入る レンダリング途中で DOM を更新すると不完全な UI が表示される
→「レンダー」と「コミット」の2フェーズに分離 レンダーフェーズ: 仮想 DOM の差分計算を行う(中断可能) DOM 操作は一切行わない コミットフェーズ: 差分計算完了後に実行(中断不可) まとめて DOM 操作を実行 11
登場するグローバル変数の説明 nextUnitOfWork : 次に処理すべき Fiber wipRoot : 作業中の Fiber のルート
currentRoot : 前回コミットされた Fiber ツリー deletions : 削除すべき Fiber のリスト 12
レンダリングフェーズ 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
コミットフェーズ function commitRoot() { // deletions: 削除すべきノードのリスト // 削除操作を最初に行う deletions.forEach(commitWork);
// wipRoot.child: ルートファイバーの子から始めて再帰的に全ての変更をコミット commitWork(wipRoot.child); // 次回の差分計算のために現在のルートを完了したルートで更新 currentRoot = wipRoot; // currentRoot: 前回コミットされたファイバーツリー // 作業用ルートをクリア(コミット完了の印) wipRoot = null; } 14
コミット作業を行う。すべてのノードを 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
4. Reconciliation DOM にレンダリングしたい内容と、前回のレンダリングの内容を比較して差分を検知 する 比較した結果、DOM に適用すべき変更があるかどうかを確認する 適用すべき変更内容の確認方法 古い Fiber
と新しい Fiber の要素が同じタイプ = 更新 古い Fiber と新しい Fiber のタイプが異なり、新しい要素がある = 新規追加 古い Fiber と新しい Fiber のタイプが異なり、古い Fiber がある = 削除 16
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
例えば、カウンターが1から2に変わった場合 <!-- 古い要素 --> <p>カウント: 1</p> <!-- 新しい要素 --> <p>カウント:
2</p> 18
レンダーフェーズ reconcileChildren 関数が実行され、同じ p タグ同士であるため、 sameType が true となる 後のコミットフェーズでの処理のために
p の Fiber の alternate や effectTag を 更新する if (sameType) { // update the node newFiber = { // ... alternate: oldFiber, effectTag: "UPDATE", }; } 19
コミットフェーズ commitRoot 関数が実行され、 p の effectTag が "UPDATE" なので updateDom
関 数が実行される if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props); // テキストノードの内容が"カウント: 2"に更新される } これにより、変更内容が DOM に反映されてユーザーにはカウンターの値が 2 と表示さ れる 20
5. hooks React が提供する API 状態を管理 useState , useReducer 作用を管理
useEffect , useLayoutEffect メモ化 useMemo , useCallback 参考 React https://speakerdeck.com/recruitengineers/react-2023?slide=108 21
useState 状態を扱うためのフック 特徴 関数コンポーネントで状態を維持することが可能になる 各コンポーネントの Fiber に配列として保存される 初期値を引数に取り、現在の状態と状態更新関数を返す setState を呼ぶと、コンポーネントの再レンダリングを発火
状態更新はすぐには適用されず、次のレンダリングサイクルで処理される 複数の useState を使って、複数の状態を管理できる 呼び出し順序に基づいてフックを識別(インデックスを使用) 22
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
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
簡易的な useEffect を実装してみる 25
ここまで学習した内容をもとに独自の useEffect フックを実装してみます。 実際の処理を追うところまでできなかったので、こんな感じかな?という部分が多く あります 26
useEffect とは? 関数コンポーネントで「副作用」を扱うためのフック 副作用: レンダリング以外の処理(データ取得、購読、DOMの直接操作など) 依存配列とエフェクト関数、クリーンアップ関数を持つ useEffect(() => { document.title
= `クリック数: ${count}`; // クリーンアップ関数(任意) return () => { console.log('コンポーネントがアンマウントされるか再レンダリングされます'); }; }, [count]); // 依存配列 27
useEffect の実装において考えること フックの情報管理 フックを Fiber に保存して再レンダリング間で状態を維持 依存配列、エフェクト関数、クリーンアップ関数、を保持 依存配列の比較 依存配列に変更があった場合のみエフェクトを実行 初回レンダリング時は無条件で実行
エフェクトの実行タイミングの管理 DOM更新(コミットフェーズ)完了後にエフェクトを実行 副作用なので、DOM が更新された後に実行する必要がある 再レンダリング時とアンマウント時にクリーンアップ関数を実行 28
フックの情報管理 // 副作用(エフェクト)をコンポーネントのレンダリング後(コミットフェーズ後)に実行するための準備をする関数 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
依存配列の比較 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
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
実行タイミングの管理 DOM更新後にエフェクトを実行 → DOMが更新されてから副作用を実行しないと、古い状態に対して操作してしまう // コミットフェーズで呼び出す関数 function commitRoot() { //
DOM への反映処理 deletions.forEach(commitWork); commitWork(wipRoot.child); // DOM更新後にエフェクトを実行 runEffects(); // 次のレンダリングサイクルの準備 currentRoot = wipRoot; wipRoot = null; } 32
エフェクトの実行と再レンダリング時のクリーンアップ処理 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
アンマウント時のクリーンアップ関数の実行 // コンポーネントがアンマウントされる際に呼び出す関数 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
実装した 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
まとめ React の内部実装の基礎知識を学ぶことができた 仮想 DOM の概念 作業単位の分割とスケジューリング (Fiber) レンダーとコミットの分離 Reconciliation
の仕組み hooks の実装方法 これによって今後のアップデートの意図や仕様の理解がスムーズになる予感 36
参考資料 Build your own React React 研修 ReactはなぜFiberで書き直されたのか?Reactの課題と将来像を探る 「仮想DOM」という用語を使わない 37