説明#
-
本文は以下の 2 点を分析します。
- useRef がどのようにデータを保存するか
- Refs がどのように実際の DOM を参照するか
-
本文を読む前に、React Hooks: hooks リストを読む必要があります。
-
バージョンは v18.1.0 に基づいています。
-
分析は以下のコードに基づいています。
import { useRef } from "react";
function UseRef() {
const ref1 = useRef(null);
const ref2 = useRef({
a: 1,
});
const handleClick = () => {
ref2.current = {
a: 3
}
}
return (
<div id="refTest" ref={ref1} onClick = {handleClick}>123</div>
)
}
export default UseRef;
TLNR#
- useRef のデータはその対応する hook オブジェクト内(memoizedState)に存在し、データの変更はページの更新を引き起こしません。
- Refs と DOM のバインディングプロセスはコミット段階のレイアウト小段階内で発生します。
useRef 解析#
mount シーンでの useRef#
mount シーンで useRef に打点を入れると、mountRef という関数に入ります。
function mountRef<T>(initialValue: T): {|current: T|} {
// hooksリストの内容を使用してhookオブジェクトを作成し、hooksリストにマウントします
const hook = mountWorkInProgressHook();
if (enableUseRefAccessWarning) {
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
} else {
// 初期値を保存するオブジェクトを作成し、初期値はuseRef内で渡された引数です
const ref = {current: initialValue};
// そのオブジェクトをhookオブジェクトのmemoizedState属性に保存します
hook.memoizedState = ref;
return ref;
}
}
物が少なく、分析することはほとんどありません。
update シーンでの useRef#
他の hooks と同様に、update 段階も存在します。コードも非常にシンプルなので、そのまま貼り付けました。
function updateRef<T>(initialValue: T): {|current: T|} {
// updateWorkInProgressHookはcurrentツリー上の対応するhookオブジェクトに基づいて
// 新しいhookオブジェクトを作成し、refの参照アドレス(memoizedStateに保存)も新しいhookのmemoizedStateに割り当てられます
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
一目瞭然のコードです。当然、useRef 内の値の変更は useState のように dispatch を呼び出す必要がないため、scheduleUpdateOnFiber も呼び出されず、この変更は React のレンダリングを引き起こしません。
まとめ#
useRef 内に渡された値は ref.current に割り当てられ、ref は useRef に対応する hook オブジェクトの memoizedState 属性にマウントされます。毎回 update 段階で新しい hook オブジェクトが作成される際に、古い hook オブジェクトの memoizedState が新しい hook オブジェクトに割り当てられるため、ref の参照アドレスが変わらず、保存された値が変わらないことが保証されます(つまり、毎回レンダリングで返される ref オブジェクトは同じオブジェクトです)。また、ref 内に保存された値の変更は React のレンダリングを引き起こしません。
Refs#
useRef の内容が非常に少ないため、例の中の ref についてもう少し話したいと思います。
ただし、ref のデバッグ思考は比較的複雑です。JSX は Babel によって React.createElement に変換されます。この関数はpackages/react/src/ReactElement.js
に存在します。この関数が返す型の宣言は次のとおりです。
{
// このタグはこれをReact要素として一意に識別することを可能にします
$$typeof: REACT_ELEMENT_TYPE,
// 要素に属する組み込みプロパティ
type: type,
key: key,
ref: ref,
props: props,
// この要素を作成する責任のあるコンポーネントを記録します。
_owner: owner,
};
明らかに ref は単独で分離されていますが、デバッグを行うには、ReactElement から fiber を作成するプロセスに目を向ける必要があります。つまり、beginWork 内です。ただし、ここでは 2 つの段階に分かれています ——1. UseRef の beginWork;2. div#refTest の beginWork。
UseRef の beginWork#
UseRef の beginWork に入ると、mountIndeterminateComponent が実行されます。
/**
* 不要なコードを多く削除しました
*/
function mountIndeterminateComponent(...) {
...
/**
* renderWithHooksはUseRef内のreturnの結果を返します
* つまり、このfunction componentが返すReactElementです
* */
value = renderWithHooks(...)
...
/**
* reconcileChildrenはReactElementを渡し、diffとfiberを生成します
*/
reconcileChildren(null, workInProgress, value, renderLanes);
}
function renderWithHooks(...) {
...
let children = Component(props, secondArg);
...
return children;
}
reconcileChildren という関数は面接の古い顔です。diff の入口はここです。ただし、ここでは diff については説明せず、ref に関係する部分(reconcileChildren -> ChildReconciler -> reconcileSingleElement)を直接見ていきます。
新しい fiber ノードを作成した後、明らかにその fiber.ref に値が割り当てられます(coerceRef が実際には ReactElement 上の ref です)。
div#refTest の beginWork#
ブレークポイントの記録に基づいて updateHostComponent に入ります。この関数内の ref に関連するコードは markRef に存在します。
function markRef(current: Fiber | null, workInProgress: Fiber) {
// ここでのrefは例内のref1です
const ref = workInProgress.ref;
/**
* 次の2つの条件のいずれかが満たされると、実行体に入ります
* 1. 初回更新時、refがnullでない
* 2. 非初回更新時、WIP上のrefとcurrent上のrefが異なる、つまり変更が発生した
*/
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Ref効果をスケジュールします
/**
* ビット操作を使用してWIPのflagsにRefをマークし、更新が発生したことを示します
*/
workInProgress.flags |= Ref;
if (enableSuspenseLayoutEffectSemantics) {
workInProgress.flags |= RefStatic;
}
}
}
markRef はその名前の通り、主に fiber に ref が存在するマークを付けるためのものです。
段階的まとめ 1#
beginWork 内の ref に関連するロジックは実際には 2 つのことを行っています。
- 新しい fiber ノードを作成した後、ReactElement 上の ref 属性を fiber.ref に割り当てます。
- 現在の遍歴中の fiber ノード上の ref が null でないか、その値が変更された場合、この fiber ノードにマークを付けます(workInProgress.flags |= Ref;)。
refs がどのように dom にバインドされるか#
明らかに refs のロジックはここで終わっていません。refs の初期化を完了しましたが、割り当てはどうでしょうか?どのように対応する DOM を refs にバインドするのでしょうか?
この問題を説明するには、公式サイトの refs に関する説明を参考にする必要があります。
React はコンポーネントがマウントされるときに current 属性に DOM 要素を渡し、コンポーネントがアンマウントされるときに null 値を渡します。ref は componentDidMount または componentDidUpdate ライフサイクルフックがトリガーされる前に更新されます。
説明がライフサイクルに関連しているので、class の例を書いてみる必要があります。
import React from "react";
class UseRef extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
console.log(this.myRef);
}
render() {
return <div ref={this.myRef} />;
}
}
export default UseRef;
componentDidMount にブレークポイントを付けると、次のようなコールスタックが見えます。
componentDidMount の呼び出しがコミット段階で発生していることが明らかにわかります(コミット段階の入口は commitRootImpl です)。コミット段階はさらに 3 つのサブ段階に分かれます。
- beforeMutation の入口は commitBeforeMutationEffects
- mutation の入口は commitMutationEffects
- layout の入口は commitLayoutEffects
これらの 3 つの段階の中で、layout 段階でのみ完全に処理された DOM 構造を取得できます。したがって、refs と DOM のバインディングの手順もここで処理するのが適しています。
実際にそうです。
refs の割り当ては commitLayoutEffects の commitLayoutEffectOnFiber 内に隠れています。
function commitLayoutEffectOnFiber(...) {
...
if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
if (enableScopeAPI) {
// TODO: これは一時的な解決策で、wwwでReact Flareから移行することを可能にしました。
/**
* finishedWork.flags & Ref
* &演算子は特徴を含むかどうかを示すことができ、特徴が含まれない場合、結果は0になります。
* 例えば、0110はすべての特徴を持ち、0010は特定の特徴を示します。
* 0110が0010という特徴を持っているかどうかを判断するには、0110 & 0010を実行し、結果が0010になります。
* 結果が0でない場合、0110は0010を含んでいると判断できます。
*/
if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
commitAttachRef(finishedWork);
}
} else {
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}
}
...
}
beginWork で fiber に付けたマークを覚えていますか?この関数はそれを使用します。flags が Ref のマークを含む場合、commitAttachRef が実行されます。
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref; // fiber上のref、現在ref.currentはnullです
if (ref !== null) {
const instance = finishedWork.stateNode; // stateNodeにはDOMオブジェクトが保存されています
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
...
if (typeof ref === 'function') {
let retVal;
...
retVal = ref(instanceToUse); // refsはコールバックでも可能です。。。
} else {
ref.current = instanceToUse; // ここがDOMをバインドするロジックで、割り当て後はrefsのDOMが最新であることが保証されます。
}
}
}
段階的まとめ 2#
refs と DOM のバインディングはコミットの layout 段階で発生します。