Slide 1

Slide 1 text

React Hooksの実装を探検してみる 株式会社カオナビ プロダクト本部 サービス開発部 Data Frontier グループ 大谷 育未

Slide 2

Slide 2 text

自己紹介 # 経歴 ● 2020年4月にカオナビに新卒入社 ● 2年目からフロントエンドエンジニアとして本格的にスタート ● 現在は機能拡充を中心としたチームにて活動中 ● 趣味はダーツ、ゲーム、モータースポーツ ○ 今年はF1がアツいです

Slide 3

Slide 3 text

目指すこと/目指さないこと # 目指すこと ● Hooksが呼び出されたときReactがなにをしているのかをざっくり理解する ● 調査の過程で出てきたReact内部の構造、キーワードを知ってもらう # 目指さないこと ● React / React Hooksの動作原理を完璧に理解する ● 登場するキーワードについて完全に理解する

Slide 4

Slide 4 text

Reactとは? ● .FUB چ'BDFCPPL ͱίϛϡχςΟʹΑͬͯ։ൃ͞Ε͍ͯΔɺ6*ߏஙͷͨΊ ͷKBWBTDSJQUϥΠϒϥϦ (JUIVC GBDFCPPLSFBDU ● ҎԼͷͭΛίϯηϓτͱͯ͠։ൃ͞Ε͍ͯΔ ○ એݴతͳ7JFX ○ ίϯϙʔωϯτϕʔε ○ Ұ౓ֶश͢Ε͹ɺͲ͜Ͱ΋࢖͑Δ ● 8FCϒϥ΢β޲͚ͷ6*։ൃʹՃ͑ɺ3FBDU/BUJWFΛ༻͍ͨϞόΠϧΞϓϦ ͷ։ൃʹ΋ར༻Ͱ͖Δ

Slide 5

Slide 5 text

React Hooksとは? ● React 16.8から追加されたReactの新機能群 ● 関数コンポーネント内でReactのstateやライフサイクルメソッドを使用する ことができる(クラスコンポーネント内では機能しない) ● ビルトインですぐに利用できるHooksが用意されている ○ useState, useEffect, useMemo etc…

Slide 6

Slide 6 text

今回のターゲットはuseEffect

Slide 7

Slide 7 text

useEffectとは? ● 関数コンポーネント内で副作用を実行することができる ○ 副作用とは? - ある処理を実行した結果他の処理に対して影響を伴うようなもの ○ stateの値を更新する、APIの値を取得するなど ● useEffectに渡された副作用関数は画面のレンダリング後に発火される ● クラスコンポーネントにおける以下のライフサイクルメソッドの代替機能 ○ componentDidMount, componentDidUpdate, componentWillUnmount

Slide 8

Slide 8 text

useEffectの本体 ● react/src/ReactHooks.js ● ReactCurrentDispatcher.current.useEffect(create, deps)を返している ○ create -> useEffectで実行させたい副作用関数 ○ deps -> 副作用関数が依存している変数の配列 // @flow export function useEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { const dispatcher = resolveDispatcher(); return dispatcher.useEffect(create, deps); } // @flow import ReactCurrentDispatcher from './ReactCurrentDispatcher'; function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; … // コメント省略 return ((dispatcher: any): Dispatcher); }

Slide 9

Slide 9 text

ReactCurrentDispatcherを見る ● ReactCurrentDispatcher.js ● currentの中身空っぽいんだけど? ○ Dispatcher型がセットされるようだが...? ○ react-reconcilerってなんだ? // @flow import type {Dispatcher} from 'react- reconciler/src/ReactInternalTypes'; const ReactCurrentDispatcher = { /** * @internal * @type {ReactComponent} */ current: (null: null | Dispatcher), }; export default ReactCurrentDispatcher;

Slide 10

Slide 10 text

React-Reconcilerってなに? ● Fiberという単位でコンポーネントの状態や更新情報を管理している ○ Fiberは一つのコンポーネントに対して基本的に1:1で存在する ○ FiberのTreeを構成し、さらに変更前と変更後の二つのTreeを比較することで、更 新が必要な差分の検出を行っている(Reconciliation) ● Reconcilerはこの差分検出処理群 ● コアロジックを共有することで、Webアプリやネイティブアプリにおける動 作の一貫性を保っている ○ ブラウザ開発開発: React + React DOM ○ モバイル向け開発: React + React Native

Slide 11

Slide 11 text

ReactにおけるFiberとは? ● コンポーネントの状態や仮想DOM情報をもつオブジェクト ● コンポーネントに対してFiberは必ず1つは存在している ● ReactはFiberを連結リストとして持つことで、コンポーネントごとに状態や 更新管理を行うことができる

Slide 12

Slide 12 text

Reconcilerが行う差分検出のイメージ Reconciliation Current WorkInProgress Time 1 Time 2

Slide 13

Slide 13 text

結局、ReactCurrentDispatcher.currentの中身は? ● renderWithHooks - ReactFiberHooks.new.js: L.373 ● currentの中身はHooksDispatcherOnMount or HooksDispatcherOnUpdate ○ 各Hookに対応した関数を持つオブジェクト ○ OnMount: L.2420 ○ OnUpdate: L. 2427 // ReactFiberHooks.new.js - L.373 … if (__DEV__) { ... } else { ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; } …

Slide 14

Slide 14 text

HooksDispatcherOnMount ● 各Hookに対応する初期化関数をもっている ● OnUpdateの場合は更新関数が割り当てら れている ○ useEffect: updateEffect const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, useContext: readContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, …, useId: mountId, unstable_isNewReconciler: enableNewReconciler, };

Slide 15

Slide 15 text

mountEffect(...) ● 特別な処理はなさそう ● mounEffectImplに渡しているフラグが気になる // ReactFiberHooks.new.jp - L.1701 function mountEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { if ( __DEV__ && … ) { return mountEffectImpl(...); } else { return mountEffectImpl( PassiveEffect | PassiveStaticEffect, HookPassive, create, deps, ); } }

Slide 16

Slide 16 text

mountEffectImpl(...) ● mountWorkInProgressHook(...) - L.635 ○ Effect Hookの状態管理ツリーに現在の状態が保存されたノ ードを追加 ○ 上記ノードが持っている情報ごと ● pushEffect(...) - L. 1543 ○ 現在のeffectの状態を保存したEffectノードを作成 し、Fiberが持っている更新キューに追加 ○ 保存される情報には依存配列の状態を含む // ReactFiberHooks.new.js - L. 1662 function mountEffectImpl(fiberFlags, hookFlags, create, deps): void { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( HookHasEffect | hookFlags, create, undefined, nextDeps, ); }

Slide 17

Slide 17 text

useEffectの初回呼び出し 1. useEffect(...)が呼び出されると最終的にmountEffectImpl(...)が呼ばれる 2. 新しいHookが実行されること、そのHookがEffect Hookによる更新であるこ とがFiberに伝えられる

Slide 18

Slide 18 text

2回目以降の動きはどうなるのか? ● useEffectは基本的に毎レンダリング呼び出される ● useEffectは引数として依存する変数の配列を渡すことができる ● 依存配列の中身が変わらない場合はuseEffectの処理は実行されない useEffect(() => { console.log(effectVal); },[effectVal]);

Slide 19

Slide 19 text

updateEffectImplに秘密が... ● 入力される依存配列が過去に使用したものと一致するかチェックしている ● 一致した場合、flagsの更新をしていない = useEffectの動作と一致している // L. 1674 const prevDeps = prevEffect.deps; if (areHookInputsEqual(nextDeps, prevDeps)) { hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.flags |= fiberFlags; // 副作用を適用するフラグを立てる

Slide 20

Slide 20 text

useEffectの実装を追ってみて ● എܠʹ͋Δ࣮૷ͷҙਤ΍։ൃνʔϜͷߟ͑ํʹ;ΕΔ͜ͱ͕Ͱ͖ͨ ● ReconcilerɺFiberͷಋೖཧ༝ͳͲ ● ίʔυΛඈͼඈͼͰ௥͏ඞཁ͕͋Δ+FiberΛར༻ͨ͠؅ཧํ๏ʹෆ׳ΕͰί ʔυΛ௥ͬͯཧղ͢Δͷ͸ΘΓͱେม ● React Hooks͸ଞʹ΋࣮૷͞Ε͍ͯΔͷͰɺڵຯ͕͋Δํ͸௥ͬͯΈͯ͸ʁ ○ ࣗ෼͸useEffectͷ࣮૷௥͏ͷͰർΕ·ͨ͠...

Slide 21

Slide 21 text

参考 ● React-Fiber-Architecture ● Reconciliation - React ● An Introduction to React Fiber - The Algorithm Behind React ● React Fiberとその周りについて調べた