Warm up
When we officially talk about useState, let's warm up first and understand the necessary knowledge.
Why are there hooks
Everyone knows that hooks are the product of function components. Why didn't there be hooks in the class component before?
The answer is simple, no need.
Because in the class component, only one instance will be generated at runtime, and the state and other information of the component will be saved in this instance. In subsequent update operations, only the render method is called, and the information in the instance will not be lost. In a function component, every rendering and update will execute this function component, so there is no way to save state and other information in a function component. In order to save information such as state, there are hooks, which are used to record the state of function components and perform side effects.
hooks execution timing
As mentioned above, in a function component, every time it is rendered, the update will execute the function component. So the hooks we declare inside the function component will also be executed every time the function component is executed.
At this time, some students may have heard what I said above (hooks are used to record the state of function components and execute side effects), and have doubts again. Since the hooks method will be executed every time a function component is executed, how does hooks record functions? What about the state of the component?
The answer is, it is recorded in the fiber node corresponding to the function component.
Two sets of hooks
When we first started learning to use hooks, there may be doubts, why should hooks be declared at the top of the function component, but not in conditional statements or inner functions?
The answer is that React maintains two sets of hooks, one is used to initialize hooks when the project initializes mount. In the subsequent update operation, the update operation will be performed based on the initialized hooks. If we declare hooks in conditional statements or functions, it may not be declared when the project is initialized, which will cause problems in subsequent update operations.
hooks storage
Let me talk about the storage method of hooks in advance, so as not to get dizzy~~~
Each initialized hook will create a hook structure. Multiple hooks are associated with the structure of the linked list through the order of declaration. Finally, the linked list will be stored in fiber.memoizedState:
copyvar hook = { memoizedState: null, // Storage hook operation, not to be confused with fiber.memoizedState baseState: null, baseQueue: null, queue: null, // Store all update operations of this hook in this update phase next: null // link next hook };
The update stored in each hook.queue is also stored in a linked list structure, so don't confuse it with the linked list of the hook.
Next, let us read the article with the following questions:
- Why can't I get the latest state value immediately after setState?
- How are multiple setState s combined?
- Is setState synchronous or asynchronous?
- Why is the function component not updated when the value of setState is the same?
Suppose we have the following piece of code:
copyfunction App(){ const [count, setCount] = useState(0) const handleClick = () => { setCount(count => count + 1) } return ( <div> brave, <span>Not afraid of difficulties</span> <span onClick={handleClick}>{count}</span> </div> ) }
Initialize the mount
useState
Let's first look at the useState() function:
copyfunction useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
The dispatcher above will involve the transformation and use of the two sets of hooks mentioned at the beginning. initialState is the parameter we pass in useState, which can be a basic data type or a function. We mainly look at the dispatcher.useState(initialState) method, because We are here to initialize, it will call the mountState method:
copyfunction mountState(initialState) { var hook = mountWorkInProgressHook(); // workInProgressHook if (typeof initialState === 'function') { // Here, if the parameter we pass in is a function, it will execute and get return as initialState initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; var queue = hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState }; var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue); return [hook.memoizedState, dispatch]; }
The above code is relatively simple, mainly to generate a queue according to the input parameters of useState() and save it in the hook, and then expose the input parameters and the dispatchAction bound with two parameters as the return value to the function component for use.
Of these two return values, the first hook.memoizedState is easier to understand, which is the initial value, and the second dispatch is dispatchAction.bind(null, currentlyRenderingFiber$1, queue) What is this?
We know that using the useState() method will return two values state, setState, this setState corresponds to the dispatchAction above, how does this function help us set the value of the state?
Let's keep this question for now, look down, and the answer will be revealed slowly later.
Next, let's mainly look at what mountWorkInProgressHook has done.
mountWorkInProgressHook
copyfunction mountWorkInProgressHook() { var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; // The if/else here is mainly used to distinguish whether it is the first hook if (workInProgressHook === null) { currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook; } else { // Add the hook to the last item of the hooks linked list, and the pointer points to this hook workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
From the above line of code currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;, we can find that the hook is stored on the corresponding fiber.memoizedState.
workInProgressHook = workInProgressHook.next = hook; From this line of code, we can know that if there are multiple hooks, they are stored in the form of a linked list.
Not only the useState() hook will use the mountWorkInProgressHook method during initialization, but other hooks, such as useEffect, useRef, useCallback, etc., will call this method during initialization.
Here we can figure out two things:
- The state data of hooks is stored in the fiber.memoizedState of the corresponding function component;
- If there are multiple hook s on a function component, they will be stored in a linked list structure in the order of declaration;
At this point, our useState() has completed all the work during its initialization. In a brief summary, useState() will store the initial value we passed in to the corresponding fiber.memoizedState in the structure of a hook during initialization, so as to The array returns [state, dispatchAction].
update update
When we trigger setState() in some form, React will also decide how to update the view based on the value of setState().
As mentioned above, useState will return [state, dispatchAction] during initialization, then we call the setState() method, which actually calls dispatchAction, and this function also binds two parameters through bind during initialization, one is useState The fiber corresponding to the function component during initialization, and the other is the queue of the hook structure.
Let's take a look at my streamlined dispatchAction (removing code that has nothing to do with setState)
copyfunction dispatchAction(fiber, queue, action) { // Create an update for subsequent updates, where the action is the input parameter of our setState var update = { lane: lane, action: action, eagerReducer: null, eagerState: null, next: null }; // Are you familiar with the operation of inserting update into this closed-loop linked list? var pending = queue.pending; if (pending === null) { update.next = update; } else { update.next = pending.next; pending.next = update; } queue.pending = update; var alternate = fiber.alternate; // Determine whether the current rendering phase if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { var lastRenderedReducer = queue.lastRenderedReducer; // A large part of this if statement is used to judge whether our update is the same as last time, and if it is the same, the scheduled update will not be performed if (lastRenderedReducer !== null) { var prevDispatcher; { prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV; } try { var currentState = queue.lastRenderedState; var eagerState = lastRenderedReducer(currentState, action); update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (objectIs(eagerState, currentState)) { return; } } finally { { ReactCurrentDispatcher$1.current = prevDispatcher; } } } } // Schedule and update the fiber carrying the update scheduleUpdateOnFiber(fiber, lane, eventTime); } }
The above code is already the result of my best simplification. . . There are comments on the code, please take a look at it.
I don't want to take a closer look to summarize what dispatchAction does:
- Create an update and add it to the fiber.hook.queue linked list, and the linked list pointer points to this update;
- Judging whether it is currently in the rendering stage and deciding whether to schedule an update immediately;
- Determine whether this operation is the same as the last operation, and if they are the same, no scheduling update will be performed;
- If the above conditions are met, the fiber with update will be scheduled for update;
Here we have figured out another problem:
Why is the function component not updated when the value of setState is the same?
Related reference video explanation: enter study
updateState
We will not explain the process of scheduling updates in detail here. For the arrangement of the following articles, here we only need to know that in the next update process, our function component will be executed again, and the useState method will be called again at this time. As mentioned earlier, React maintains two sets of hooks, one for initialization and one for update. This has already been switched when scheduling updates. So our call to the useState method this time will be different from the previous initialization.
This time we enter useState, we will see that the updateState method is actually called
copyfunction updateState(initialState) { return updateReducer(basicStateReducer); }
Seeing these lines of code, readers should understand why some people on the Internet say that useState is similar to useReducer. It turns out that updateReducer is called in the update of useState.
updateReducer
It was very long, so I want everyone to bear with it. I can't bear it, the pain has reduced a lot
copyfunction updateReducer(reducer, initialArg, init) { // Create a new hook with update created by dispatchAction var hook = updateWorkInProgressHook(); var queue = hook.queue; queue.lastRenderedReducer = reducer; var current = currentHook; var baseQueue = current.baseQueue; var pendingQueue = queue.pending; current.baseQueue = baseQueue = pendingQueue; if (baseQueue !== null) { // Can you see the benefits of creating a closed-loop linked list and inserting update from here? You can find the first update directly next var first = baseQueue.next; var newState = current.baseState; var update = first; // Start traversing the update linked list to execute all setState do { var updateLane = update.lane; // If there are multiple setState s on our update, in the loop process, the merge operation will eventually be performed var action = update.action; // The reducer here will judge the action type, as follows newState = reducer(newState, action); update = update.next; } while (update !== null && update !== first); hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } var dispatch = queue.dispatch; return [hook.memoizedState, dispatch]; }
In the above update, it will loop through the update to perform a merge operation, and only take the last setState value. At this time, some people may ask, isn’t it more convenient to directly take the last setState value?
This is not possible, because the setState input parameter can be a basic type or a function. If the input is a function, it will rely on the value of the previous setState to complete the update operation. The following code is the reducer in the above loop
copyfunction basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; }
So far we have figured out a question, how are multiple setState s merged?
updateWorkInProgressHook
The following is the pseudo-code. I deleted a lot of logical judgments, so as not to be too long and make everyone feel uncomfortable. The original code will judge whether the current hook is the first hook to schedule an update. I am here for simplicity. parse by first
copyfunction updateWorkInProgressHook() { var nextCurrentHook; nextCurrentHook = current.memoizedState; var newHook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null } currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook; return workInProgressHook; }
As can be seen from the above code, updateWorkInProgressHook throws away those judgments, and actually does something very simple, that is, to create a new hook structure based on fiber.memoizedState to overwrite the previous hook. The dispatchAction mentioned above will add the update to the hook.queue, and there is this update on the newHook.queue here.
Summarize
To summarize useState initialization and setState update:
- useState will be initialized when the function component is executed for the first time, returning [state, dispatchAction].
- When we schedule updates through setState, that is, dispatchAction, an update will be created and added to hook.queue.
- When the function component is executed again during the update process, the useState method will also be called. At this time, useState will use the updated hooks internally.
- Get the update created by dispatchAction through updateWorkInProgressHook.
- In the updateReducer, the setState merge is completed by traversing the update linked list.
- Return [newState, dispatchAction] after update.
two more questions
- Why can't I get the latest state value immediately after setState? React can actually do this, why not do it, because each setState will trigger an update, and React will do a merge operation for performance reasons. So setState just triggers dispatchAction to generate an update action, the new state will be stored in the update, and the new state will not be assigned until the next render triggers the execution of the function component where the useState is located.
- Is setState synchronous or asynchronous? Synchronously, if we have a piece of code like this:
copyconst handleClick = () => { setCount(2) setCount(count => count + 1) console.log('after setCount') }
You will be surprised to find that the page has not updated the count, but the console has printed after setCount.
The reason why it looks asynchronous is because try{...}finally{...} is used internally. When calling setState to trigger a scheduling update, the update operation will be placed in finally, and return to continue to execute the logic of handlelick. So the above situation will appear.
After reading this article, we can figure out the following questions:
- Why can't I get the latest state value immediately after setState?
- How are multiple setState s combined?
- Is setState synchronous or asynchronous?
- Why is the function component not updated when the value of setState is the same?
- How does setState complete the update?
- When is useState initialized and when is it updated?