Before you begin, tell me what problems the article will help you solve.
-
What are React Hooks?
-
How do React Hooks work? React source code
- React hooks vs. Class Component
- How do React hooks save state
- Why is setState “asynchronous”
If you have any questions about Fiber, you can read the previous article
React Advanced Front-end Interview –React Fiber
preface
The source code for this article is based on React V17.0.2
PS. Use useState as an example to introduce the process of React Hooks. Other Hooks also belong to this process, but have their own logic processing
What are React Hooks?
In a class component, only one class instance is generated at a time, and the component state is stored in the instance. Each subsequent update does not lose state in the class by simply calling the Render method of the class. But if it is a function component, the function needs to be re-executed every render update, and the function component has no ability to save state. So up until now we’ve treated the function component as a pure presentation component, updating the page through props. So Hooks give function components the ability to save data state and perform Side Effects.
How do React Hooks save the last data state since the function renders from a new start each time?
How do React Hooks work?
Virtual DOM has been improved since react appeared. It is known that the virtual DOM uses JS data to store THE DOM structure. Every data update is to update the JS object, and the page is updated by mapping the object to the DOM. All dom is a data structure in JS, so all components are also data structures. In React, it is called Fiber, and function components are also called Fiber nodes. If you store the state of function components on Fiber nodes, The data state can be saved by fetching data from Fiber every time a function is executed. All components are essentially a data structure of a Fiber node, and all data can be stored in Fiber.
What do hooks look like?
Let’s fast forward right to the Hook’s data structure
reactFiberHooks.new.js function mountWorkInProgressHook(): Hook { const hook: Hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; If (workInProgressHook === null) {// This is the first hook in the list // We can see the state stored on Fiber's memoizedState currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else {// Append to the end of the list // next = hook; } return workInProgressHook; }Copy the code
In the React source code, each time useState is created, a new hook is created and mounted to the current hook.next, so all states of the function components are stored in Fiber’s memoizedState as a single linked list. This also explains why React officially warns against using hooks in conditional judgments.
How do hooks update data?
Remember? React executes function every time it updates. But for us users, useState () is executed in both initialization and update, as follows:
const [num,setNum] = use(0)
Copy the code
Inside React, all initializations and updates are two sets of code
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
Copy the code
Corresponding to: mountState and updateState; initialization stage; initialize hook; create the above hook single linked list; updateState data in update stage
Initialization phase
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 = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
(dispatchAction.bind(null, currentlyRenderingFiber, queue): any));
return [hook.memoizedState, dispatch];
}
Copy the code
Call to useState initialization will execute mountState and return [hook.memoizedState, dispatch]. See what it does (the code below removes a lot of extraneous code)
function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A ) { const update: Update<S, A> = { action, eagerReducer: null, eagerState: null, next: (null: any), }; const alternate = fiber.alternate; const pending = queue.pending; if (pending === null) { // This is the first update. Create a circular list. update.next = update; } else { update.next = pending.next; pending.next = update; } queue.pending = update; // scheduleUpdateOnFiber(fiber); }}Copy the code
DispatchAction receives 3 parameters, Fiber, Queue and action, but why did setNum pass in only one action? Bind (null, currentlyRenderingFiber, Queue) to pass in the current fiber and queue, so when setNum passes in the action, A dispatchACtion is performed, an Update queue is constructed, and fiber’s scheduling mechanism (the same mechanism used by class’s setState) triggers the update. If the update starts, it goes to the next render, which re-executes function. Then you go to useState, which is the update estate phase.
Update the stage
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
Copy the code
UpdateState is essentially an updateReducer (so updateState is just a simple version of updateReducer, both in use and implementation). So jump straight to updateReducer(remove scheduling and other irrelevant code)
function updateReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init? : (I) => S ): [S, Dispatch<A>] { const hook = updateWorkInProgressHook(); const queue = hook.queue; queue.lastRenderedReducer = reducer; const current: Hook = (currentHook: any); // The last rebase update that is NOT part of the base state. let baseQueue = current.baseQueue; if (baseQueue ! == null) { // We have a queue to process. const first = baseQueue.next; let newState = current.baseState; let newBaseState = null; let newBaseQueueFirst = null; let newBaseQueueLast = null; let update = first; Do {// Call the incoming reducer update state const action = update.action; newState = reducer(newState, action); update = update.next; } while (update ! == null && update ! == first); if (newBaseQueueLast === null) { newBaseState = newState; } else { newBaseQueueLast.next = (newBaseQueueFirst: any); } if (! is(newState, hook.memoizedState)) { markWorkInProgressReceivedUpdate(); } hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } const dispatch: Dispatch<A> = (queue.dispatch: any); return [hook.memoizedState, dispatch]; }Copy the code
Merge the update queue to calculate the new state. This is why setNum is “asynchronous”
conclusion
-
The Hooks store state in a single linked list in the corresponding Fiber structure, which is read sequentially with each update.
-
SetState is “asynchronous” because React stores the state in a queue and waits for it to be updated when scheduling is complete
-
Almost all of the Hooks used related questions and answers can be seen in the React website FAQ, strongly recommended reading (may encounter problems in the interview, such as how usePrevious implementation, useMemo, useRef, etc…).
-
It feels like too much source code (even though it’s already simplified) will affect the reading experience
-
The source code section of the article can be found here