React源码解读
React 源码解读一直是我想要做但是确实缺失的地方。因为读起来实在是太晦涩了,我会通过一些时间片段来一步步记录是怎么理解 React 源码,并为我理解React的原理做地基。
入口文件
入口文件很简单就是 packages/react/index 在这里我通过选择通过 useState 去窥探 React 的刷新原理
// packages/react/src
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
很简单,获取一个Dispatcher,然后返回其中的 useState 方法,并传入 initialState 值,那么 resolveDispatcher 是什么呢?
// packages/react/src/ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
if (__DEV__) {
// ...
}
//如果在渲染阶段之外访问,将导致空访问错误。我们
//故意不要抛出我们自己的错误,因为这是在一个热点路径上。
//还有助于确保这是内联的。
return ((dispatcher: any): Dispatcher);
}
这里我们只能通过类型 Dispatcher 去寻找那么就来到了 packages/react-reconciler 这个包下 reconciler 就是调和的意思。其中有三个对象均返回 Dispatcher 类型。分别是ContextOnlyDispatcher
,HooksDispatcherOnMount
,HooksDispatcherOnUpdate
,HooksDispatcherOnRerender
。其中ContextOnlyDispatcher
为报错方法。其余三个对象根据名字可以推断分别是 挂载中、更新、渲染的三个对象。我决定先看HooksDispatcherOnMount
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
if (__DEV__) {
hookTypesDev =
current !== null
? ((current._debugHookTypes: any): Array<HookType>)
: null;
hookTypesUpdateIndexDev = -1;
// Used for hot reloading:
ignorePreviousDependencies =
current !== null && current.type !== workInProgress.type;
}
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// thenableIndexCounter = 0;
// thenableState = null;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so memoizedState would be null during updates and mounts.
if (__DEV__) {
if (current !== null && current.memoizedState !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
// This dispatcher handles an edge case where a component is updating,
// but no stateful hooks have been used.
// We want to match the production code behavior (which will use HooksDispatcherOnMount),
// but with the extra DEV validation to ensure hooks ordering hasn't changed.
// This dispatcher does that.
ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
}
} else {
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
// In Strict Mode, during development, user functions are double invoked to
// help detect side effects. The logic for how this is implemented for in
// hook components is a bit complex so let's break it down.
//
// We will invoke the entire component function twice. However, during the
// second invocation of the component, the hook state from the first
// invocation will be reused. That means things like `useMemo` functions won't
// run again, because the deps will match and the memoized result will
// be reused.
//
// We want memoized functions to run twice, too, so account for this, user
// functions are double invoked during the *first* invocation of the component
// function, and are *not* double invoked during the second incovation:
//
// - First execution of component function: user functions are double invoked
// - Second execution of component function (in Strict Mode, during
// development): user functions are not double invoked.
//
// This is intentional for a few reasons; most importantly, it's because of
// how `use` works when something suspends: it reuses the promise that was
// passed during the first attempt. This is itself a form of memoization.
// We need to be able to memoize the reactive inputs to the `use` call using
// a hook (i.e. `useMemo`), which means, the reactive inputs to `use` must
// come from the same component invocation as the output.
//
// There are plenty of tests to ensure this behavior is correct.
const shouldDoubleRenderDEV =
__DEV__ &&
debugRenderPhaseSideEffectsForStrictMode &&
(workInProgress.mode & StrictLegacyMode) !== NoMode;
shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;
let children = Component(props, secondArg);
shouldDoubleInvokeUserFnsInHooksDEV = false;
// Check if there was a render phase update
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// Keep rendering until the component stabilizes (there are no more render
// phase updates).
children = renderWithHooksAgain(
workInProgress,
Component,
props,
secondArg,
);
}
if (shouldDoubleRenderDEV) {
// In development, components are invoked twice to help detect side effects.
setIsStrictModeForDevtools(true);
try {
children = renderWithHooksAgain(
workInProgress,
Component,
props,
secondArg,
);
} finally {
setIsStrictModeForDevtools(false);
}
}
finishRenderingHooks(current, workInProgress);
return children;
}
// packages/react-reconciler/src/ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
};
查看导出的 useState,也就是 updateState
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
(dispatchSetState.bind(null, currentlyRenderingFiber, queue): any));
return [hook.memoizedState, dispatch];
}
function updateFunctionComponent(
current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
// Lazy component props can't be validated in createElement
// because they're only guaranteed to be resolved here.
const innerPropTypes = Component.propTypes;
if (innerPropTypes) {
checkPropTypes(
innerPropTypes,
nextProps, // Resolved props
'prop',
getComponentNameFromType(Component),
);
}
}
}
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
context = getMaskedContext(workInProgress, unmaskedContext);
}
let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
setIsRendering(true);
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
setIsRendering(false);
} else {
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}