Reactの内部構造を見ていくよ(初回マウントrender関数呼び出しからのトリガーフェーズ)
参考にする
<html>
<body>
(ここは省略)
<div id="root"><!-- ここにReactの結果が入る --></div>
<script>
const App = () => {
const child = React.createElement('span', null, 'world');
return React.createElement('div', null, 'Hello', child);
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(React.createElement(App, {}, null));
</script>
スクリプトを抜粋すると以下の通り
const App = () => {
const child = React.createElement('span', null, 'world');
return React.createElement('div', null, 'Hello', child);
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(React.createElement(App, {}, null));
なおこの例ではJSX(TSX)を利用せずcreateElementでエレメント生成を直接記述している
createRoot関数の中身
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
if (!isValidContainer(container)) {
throw new Error('Target container is not a DOM element.');
}
warnIfReactDOMContainerInDEV(container);
const concurrentUpdatesByDefaultOverride = false;
let isStrictMode = false;
let identifierPrefix = '';
let onUncaughtError = defaultOnUncaughtError;
let onCaughtError = defaultOnCaughtError;
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
if (options !== null && options !== undefined) {
if (__DEV__) {
if ((options: any).hydrate) {
console.warn(
'hydrate through createRoot is deprecated. Use ReactDOMClient.hydrateRoot(container, <App />) instead.',
);
} else {
if (
typeof options === 'object' &&
options !== null &&
(options: any).$$typeof === REACT_ELEMENT_TYPE
) {
console.error(
'You passed a JSX element to createRoot. You probably meant to ' +
'call root.render instead. ' +
'Example usage:\n\n' +
' let root = createRoot(domContainer);\n' +
' root.render(<App />);',
);
}
}
}
if (options.unstable_strictMode === true) {
isStrictMode = true;
}
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
if (options.onUncaughtError !== undefined) {
onUncaughtError = options.onUncaughtError;
}
if (options.onCaughtError !== undefined) {
onCaughtError = options.onCaughtError;
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
if (options.unstable_transitionCallbacks !== undefined) {
transitionCallbacks = options.unstable_transitionCallbacks;
}
}
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
return new ReactDOMRoot(root);
}
createContainer関数では何を作っている?
rootということはFiberRootNodeを作っている?
createContainer関数はreact-reconcilerの内部にある
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
// TODO: Remove `concurrentUpdatesByDefaultOverride`. It is now ignored.
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onUncaughtError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
onCaughtError: (
error: mixed,
errorInfo: {
+componentStack?: ?string,
+errorBoundary?: ?React$Component<any, any>,
},
) => void,
onRecoverableError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
null,
);
}
createFiberRoot、文字通りの関数が出てきました
createFiberRoot関数
export function createFiberRoot(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
// TODO: We have several of these arguments that are conceptually part of the
// host config, but because they are passed in at runtime, we have to thread
// them through the root constructor. Perhaps we should put them all into a
// single type, like a DynamicHostConfig that is defined by the renderer.
identifierPrefix: string,
onUncaughtError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
onCaughtError: (
error: mixed,
errorInfo: {
+componentStack?: ?string,
+errorBoundary?: ?React$Component<any, any>,
},
) => void,
onRecoverableError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
formState: ReactFormState<any, any> | null,
): FiberRoot {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
formState,
): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
if (enableTransitionTracing) {
root.transitionCallbacks = transitionCallbacks;
}
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
if (enableCache) {
const initialCache = createCache();
retainCache(initialCache);
// The pooledCache is a fresh cache instance that is used temporarily
// for newly mounted boundaries during a render. In general, the
// pooledCache is always cleared from the root at the end of a render:
// it is either released when render commits, or moved to an Offscreen
// component if rendering suspends. Because the lifetime of the pooled
// cache is distinct from the main memoizedState.cache, it must be
// retained separately.
root.pooledCache = initialCache;
retainCache(initialCache);
const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate,
cache: (null: any), // not enabled yet
};
uninitializedFiber.memoizedState = initialState;
}
initializeUpdateQueue(uninitializedFiber);
return root;
}
とりあえずあまり深入りせず
FiberRootNodeを作成しているということを推測
また、ダミーのHostRootを作成して適当に充てがっている?
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
FiberRootNodeはルートとして機能する特別なノードで、アプリ全体に関する必要なメタ情報を保持します。そのcurrentがFiber Treeの実体を指し、新しいFiber Treeが構築されるとcurrentが新しいHostRootを指し直します。
createRootの中でFiberRootNodeに加えてダミーのHostRootを作成し、FiberRootNodeのcurrentをダミーのHostRootへ向けています。
とりあえず見たいものはみたので、createRoot関数に関する追跡を終了
render関数の調査に移る
renderの部分
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
// $FlowFixMe[missing-this-annot]
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
if (__DEV__) {
if (typeof arguments[1] === 'function') {
console.error(
'does not support the second callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
);
} else if (isValidContainer(arguments[1])) {
console.error(
'You passed a container to the second argument of root.render(...). ' +
"You don't need to pass it again since you already passed it to create the root.",
);
} else if (typeof arguments[1] !== 'undefined') {
console.error(
'You passed a second argument to root.render(...) but it only accepts ' +
'one argument.',
);
}
}
updateContainer(children, root, null, null);
};
コンテナが有効ならコンテナ上書きする
updateContainer関数について調査する
updateContainer関数
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
updateContainerImpl(
current,
lane,
element,
container,
parentComponent,
callback,
);
return lane;
}
updateContainerImplに実際の実装が入ってくる感じか
そういえば、useStateの内部を調査したときも
requestUpdateLane関数でlaneを要求してたな・・・
似たような処理を回しているのだろうと推測
updateContainerImpl関数
function updateContainerImpl(
rootFiber: Fiber,
lane: Lane,
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): void {
if (__DEV__) {
onScheduleRoot(container, element);
}
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
if (__DEV__) {
if (
ReactCurrentFiberIsRendering &&
ReactCurrentFiberCurrent !== null &&
!didWarnAboutNestedUpdates
) {
didWarnAboutNestedUpdates = true;
console.error(
'Render methods should be a pure function of props and state; ' +
'triggering nested component updates from render is not allowed. ' +
'If necessary, trigger nested updates in componentDidUpdate.\n\n' +
'Check the render method of %s.',
getComponentNameFromFiber(ReactCurrentFiberCurrent) || 'Unknown',
);
}
}
const update = createUpdate(lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
if (__DEV__) {
if (typeof callback !== 'function') {
console.error(
'Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callback,
);
}
}
update.callback = callback;
}
const root = enqueueUpdate(rootFiber, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, rootFiber, lane);
entangleTransitions(root, rootFiber, lane);
}
}
- レーンを元にupdateオブジェクトを作成
- (これってuseStateのときと同じことをやってそう)
- キューにupdateオブジェクトを追加
- rootがnullでないならscheduleUpdateOnFiberとentangleTransitionUpdateを実行
const root = enqueueUpdate(rootFiber, update, lane); if (root !== null) { scheduleUpdateOnFiber(root, rootFiber, lane); entangleTransitions(root, rootFiber, lane); }
やってることはuseStateのdispatchの中身とそっくりだ
念の為enqueueUpdate関数を調査
export function enqueueUpdate<State>(
fiber: Fiber,
update: Update<State>,
lane: Lane,
): FiberRoot | null {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return null;
}
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
if (__DEV__) {
if (
currentlyProcessingQueue === sharedQueue &&
!didWarnUpdateInsideUpdate
) {
const componentName = getComponentNameFromFiber(fiber);
console.error(
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
'from inside an update function. Update functions should be pure, ' +
'with zero side-effects. Consider using componentDidUpdate or a ' +
'callback.\n\nPlease update the following component: %s',
componentName,
);
didWarnUpdateInsideUpdate = true;
}
}
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
// This is an unsafe render phase update. Add directly to the update
// queue so we can process it immediately during the current render.
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
// Update the childLanes even though we're most likely already rendering
// this fiber. This is for backwards compatibility in the case where you
// update a different component during render phase than the one that is
// currently renderings (a pattern that is accompanied by a warning).
return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
} else {
return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}
}
fiberの持っているupdateQueueに追加しているっぽい
useStateのときはhookオブジェクトのupdateQueueにupdateオブジェクトを追加していたけども 違うかも
render関数の場合はfiberオブジェクトがupdateQueueを持っているってことか?
あと、ここの部分はクラスコンポーネントか関数コンポーネントかで変わってくるっぽい
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
// This is an unsafe render phase update. Add directly to the update
// queue so we can process it immediately during the current render.
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
// Update the childLanes even though we're most likely already rendering
// this fiber. This is for backwards compatibility in the case where you
// update a different component during render phase than the one that is
// currently renderings (a pattern that is accompanied by a warning).
return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
} else {
return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}
ここ以降はレンダーフェーズ編を参考に