React source code has been simplified to remove the logic related to static type and fiber, and retain the most essential Hook logic, hope to help you
preface
Since React Hooks was released, the community has embraced it and learned from it in a positive way. There have also been many articles about React Hooks source code parsing. This article (based on V16.8.6) to the author’s own point of view to write an article of their own. React Hooks: React-hooks: React-hooks: React-hooks: React-hooks This article will present the content in the form of text, code, and pictures. This paper mainly studies useState, useReducer and useEffect of Hooks, and tries to uncover Hooks as much as possible.
Confusion when using Hooks
The release of Hooks allows our Function Component to progressively have the same features as the Class Component, such as private states, lifecycle functions, etc. UseState and useReducer Hooks allow us to use private state in Function Component. UseState is actually a castrated version of useReducer, which is why I put the two together. Use the official example:
function PersionInfo ({initialAge,initialName}) {
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
return (
<>
Age: {age}, Name: {name}
<button onClick={()= > setAge(age + 1)}>Growing up</button>
</>
);
}
Copy the code
UseState initializes a private state, which returns the latest value of the state and a method to update the state. UseReducer is for more complex state management scenarios:
const initialState = {age: 0.name: 'Dan'};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return{... state,age: state.age + action.age};
case 'decrement':
return{... state,age: state.age - action.age};
default:
throw new Error();
}
}
function PersionInfo() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Age: {state.age}, Name: {state.name}
<button onClick={()= > dispatch({type: 'decrement', age: 1})}>-</button>
<button onClick={()= > dispatch({type: 'increment', age: 1})}>+</button>
</>
);
}
Copy the code
Also returns the current status and a method used to update the data. When using these two methods, we might think of the following question:
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
Copy the code
How does React internally differentiate between these two states? Unlike the Class Component, a Function Component does not mount a private state to a Class instance and use a key to point to that state, and every page refresh or Component re-rendering causes the Function to be reexecuted. So React must have a mechanism to distinguish between these Hooks.
const [age, setAge] = useState(initialAge);
/ / or
const [state, dispatch] = useReducer(reducer, initialState);
Copy the code
Another question is how React returns to the latest state each time it is re-rendered. Class Component has the ability to persistently mount private state to a Class instance, keeping the latest value at all times. The Function Component is essentially a Function and is re-executed every time it is rendered. React must have some mechanism to remember every update and eventually return the latest value. Of course, there are other questions, like where are these states stored? Why use Hooks only at the top of functions and not in conditional statements, etc.?
The answer lies in the source code
Let’s look at the source code implementation for useState and useReducer and answer some questions about using Hooks. Let’s start at the source:
import React, { useState } from 'react';
Copy the code
We usually introduce the useState method in this way in a project. What does the useState method look like when we introduce it? This method is actually the source packages/react/SRC/ReactHook. Js.
// packages/react/src/ReactHook.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// ...
return dispatcher;
}
// The useState method introduced in our code
export function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState)
}
Copy the code
Can see from the source, we call it is ReactCurrentDispatcher dispatcher in the js. UseState (), then we go to ReactCurrentDispatcher. Js files:
import type {Dispacther} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
Copy the code
Well, it will continue to react – we take the reconciler/SRC/ReactFiberHooks js this file. So let’s go ahead to this file.
// react-reconciler/src/ReactFiberHooks.js
export type Dispatcher = {
useState<S>(initialState: (() = > S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) = > S,
initialArg: I, init? :(I) = > S,
): [S, Dispatch<A>],
useEffect(
create: (a)= > ((a)= > void) | void.deps: Array<mixed> | void | null,
): void.// Other hooks type definitions
}
Copy the code
Cruise around we finally know the React Hooks source put the React – the reconciler/SRC/ReactFiberHooks js directories below. Here we can see the type definition for each Hooks as shown above. You can also see the Hooks implementation in this file. First we notice that most of our Hooks have two definitions:
// react-reconciler/src/ReactFiberHooks.js
// Definition of the Mount phase Hooks
const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect,
useReducer: mountReducer,
useState: mountState,
/ / other Hooks
};
// Definition of Hooks in the Update phase
const HooksDispatcherOnUpdate: Dispatcher = {
useEffect: updateEffect,
useReducer: updateReducer,
useState: updateState,
/ / other Hooks
};
Copy the code
As you can see here, the logic of our Hooks is different in the Mount and Update phases. They are two different definitions in the Mount phase and the Update phase. Let’s start with the logic of the Mount phase. Let’s think about a few things before we look at them. What do React Hooks need to do in Mount phase? Take our useState and useReducer:
- We need to initialize the state and return methods that modify the state, which is basic.
- We need to manage each Hooks separately.
- Provide a data structure to hold the update logic so that each subsequent update gets the latest value.
Let’s look at the React implementation. Let’s look at the mountState implementation first.
// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
// Get the current Hook node and add the current Hook to the list of hooks
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// Declare a linked list to store updates
const queue = (hook.queue = {
last: null.dispatch: null,
lastRenderedReducer,
lastRenderedState,
});
// Returns a dispatch method to modify the status and add the update to the Update list
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// Returns the current state and the method to modify the state
return [hook.memoizedState, dispatch];
}
Copy the code
Distinguish management Hooks
For the first thing, methods to initialize and return state and update state. InitialState is used to initialize the state and return the state and the corresponding update method return [hook.memoizedState, dispatch]. React differentiates between Hooks. Use the mountWorkInProgressHook method in mountState and the type definition of the Hook.
// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null.queue: UpdateQueue<any, any> | null.next: Hook | null.// Point to the next Hook
};
Copy the code
Remember that React defines Hooks as a linked list. That is, the Hooks we use in the component are linked by a linked list, and the next of the previous Hooks points to the next Hooks. How are these Hooks connected in series using a linked list data structure? The logic is in the mountWorkInProgressHook method called by the Hooks function for each specific mount phase:
// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook() :Hook {
const hook: Hook = {
memoizedState: null.baseState: null.queue: null.baseUpdate: null.next: null};if (workInProgressHook === null) {
// The current workInProgressHook list is empty.
// use the current Hook as the first Hook
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// Otherwise add the current Hook to the end of the Hook list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
Copy the code
In the mount phase, whenever we call the Hooks method, such as useState, mountState calls the mountWorkInProgressHook to create a Hook node and add it to the Hooks list. Take our example:
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect((a)= > {})
Copy the code
In the mount phase, a single linked list like the one shown below is produced:
Returns the latest value
On the third, useState and useReducer both use a queue to store each update. So that a later update phase can return the latest status. Every time we call the dispatchAction method, a new updata object is created and added to the queue, and this is a circular list. Take a look at the implementation of the dispatchAction method:
// react-reconciler/src/ReactFiberHooks.js
// Remove special cases and fiber-related logic
function dispatchAction(fiber,queue,action,) {
const update = {
action,
next: null};// Add the update object to the circular list
const last = queue.last;
if (last === null) {
// The list is empty, the current update is first, and the loop is kept
update.next = update;
} else {
const first = last.next;
if(first ! = =null) {
// Insert a new update object after the latest update object
update.next = first;
}
last.next = update;
}
// Keep the table header on the latest update object
queue.last = update;
// Perform scheduling tasks
scheduleWork();
}
Copy the code
So every time we do a dispatchAction method like setAge or setName. An Update object holding the update is created and added to the update queue. Then each Hooks node will have its own queque. For example, suppose we execute the following statements:
setAge(19);
setAge(20);
setAge(21);
Copy the code
Our Hooks list would look like this:
// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init,) {
// Get the current Hook node and add the current Hook to the list of hooks
const hook = mountWorkInProgressHook();
let initialState;
/ / initialization
if(init ! = =undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg ;
}
hook.memoizedState = hook.baseState = initialState;
// Store a list of update objects
const queue = (hook.queue = {
last: null.dispatch: null.lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
// Returns a dispatch method to modify the status and add the update to the Update list
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// Return state and methods to modify state
return [hook.memoizedState, dispatch];
}
Copy the code
Then we look at the Update phase, where we see how our useState or useReducer uses the existing information to return us the latest and most correct values. Let’s take a look at the useState code in the update phase, which is called updateState:
// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
Copy the code
The reducerEducer reducer will not be passed when useState is called, so a basicStateReducer will be passed by default. BasicStateReducer basicStateReducer
// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
return typeof action === 'function' ? action(state) : action;
}
Copy the code
When useState(Action) is used, the action is usually a value, not a method. So what the Base atereducer is really going to do is return this action. Let’s continue with the updateReducer logic:
// react-reconciler/src/ReactFiberHooks.js
// Remove the logic related to fiber
function updateReducer(reducer,initialArg,init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// Get the header of the update list
const last = queue.last;
// Get the original update objectfirst = last ! = =null ? last.next : null;
if(first ! = =null) {
let newState;
let update = first;
do {
// Perform each update to update the status
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while(update ! = =null&& update ! == first); hook.memoizedState = newState; }const dispatch = queue.dispatch;
// Returns the latest status and the method to modify the status
return [hook.memoizedState, dispatch];
}
Copy the code
In the update phase, which is the second and third time for our component. When executing to useState or useReducer, we iterate over the list of update objects, performing each update to calculate the latest state and return it to ensure that we get the latest state every time we refresh the component. The reducer of useState is a baseStateReducer, because the update. Action passed in is a value, update. Action is directly returned, while the reducer of useReducer is a user-defined reducer. So the latest state is calculated step by step based on the incoming action and the newState from each loop.
UseState/useReducer small summary
Here we go back to some of the initial questions:
-
React how to manage distinguishes Hooks?
- React manages Hooks through a single linked list
- Add Hook nodes to the linked list in the order in which Hooks are executed
-
How do useState and useReducer return the latest value every time they render?
- Each Hook node performs all update operations through a circular list
- In the update phase, all the update operations in the update loop are performed, and the latest state is returned
-
Why can’t you use Hooks in conditional statements, etc?
- The list!
useState(‘A’), useState(‘B’), useState(‘C’)
useState(‘B’)
Where is the Hooks list?
Okay, now that we know React manages Hooks via a linked list, it also has a circular linked list that holds every update operation so that the latest state is calculated and returned to us every time a component updates. So where do we keep this Hooks list? Of course we need to store it somewhere relative to the current component. So the obvious one to one component is our FiberNode.
As shown, the component builds the Hooks list that is mounted on the FiberNode memoizedState.
useEffect
Seeing this, I believe you already have some knowledge of the Hooks source code implementation pattern, so try to see the Effect implementation and you will immediately understand. First let’s recall how useEffect works.
function PersionInfo () {
const [age, setAge] = useState(18);
useEffect((a)= >{
console.log(age)
}, [age])
const [name, setName] = useState('Dan');
useEffect((a)= >{
console.log(name)
}, [name])
return (
<>.</>
);
}
Copy the code
The PersionInfo component outputs age and name to the console for the first rendering, and the state of useEffect deps dependencies on the console for every subsequent update. The cleanup function (if any) is also executed when unmounted. How does React implement this? In FiberNode, all the effects are stored in a single updateQueue, and all the effects that need to be executed are executed in sequence after each render. UseEffect is also divided into mountEffect and updateEffect
mountEffect
// react-reconciler/src/ReactFiberHooks.js
// Simplify to remove special logic
function mountEffect( create,deps,) {
return mountEffectImpl(
create,
deps,
);
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// Get the current Hook and add the current Hook to the Hook list
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// Save the current effect to the memoizedState property of the Hook node,
// and add to fiberNode update Ue
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// ComponentUpdateue is mounted to fiberNode updateQueue
if (componentUpdateQueue === null) {
If Queue is empty, effect is the first node
componentUpdateQueue = createFunctionComponentUpdateQueue();
// Keep the loop
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// Otherwise, add to the current Queue
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
constfirstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; }}return effect;
}
Copy the code
As you can see in the mount phase, all useEffect does is add its own effect to component update ue. This componentUpdateQueue is assigned to the fiberNode updateQueue in the renderWithHooks method.
// react-reconciler/src/ReactFiberHooks.js
// Simplify to remove special logic
export function renderWithHooks() {
const renderedWork = currentlyRenderingFiber;
renderedWork.updateQueue = componentUpdateQueue;
}
Copy the code
During the mount phase, all of our effects were attached to fiberNode as a linked list. Then, after the component is rendered, React executes all the methods in update Ue.
updateEffect
// react-reconciler/src/ReactFiberHooks.js
// Simplify to remove special logic
function updateEffect(create,deps){
return updateEffectImpl(
create,
deps,
);
}
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
// Get the current Hook node and add it to the Hook list
const hook = updateWorkInProgressHook();
/ / rely on
const nextDeps = deps === undefined ? null : deps;
// Clear the function
let destroy = undefined;
if(currentHook ! = =null) {
// Get the effect of the previous rendering of the Hook node
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if(nextDeps ! = =null) {
const prevDeps = prevEffect.deps;
// Compare dePS dependencies
if (areHookInputsEqual(nextDeps, prevDeps)) {
// If the dependency does not change, the NoHookEffect tag is tagged, which is skipped during the commit phase
// effect execution
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
Copy the code
The update phase is similar to the mount phase, but this time the effect dependency dePS is considered. If the effect dependency does not change, the NoHookEffect tag will be applied, and the effect execution will be skipped during the commit phase.
function commitHookEffectList(unmountTag,mountTag,finishedWork) {
const updateQueue = finishedWork.updateQueue;
letlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
if(lastEffect ! = =null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if((effect.tag & unmountTag) ! == NoHookEffect) {// Unmount tag! == NoHookEffect effect removal function (if any)
const destroy = effect.destroy;
effect.destroy = undefined;
if(destroy ! = =undefined) { destroy(); }}if((effect.tag & mountTag) ! == NoHookEffect) {// The Mount phase executes all tags! == NoHookEffect effect. Create,
// Our cleanup function (if any) is returned to the destroy property, unmount it once
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
Copy the code
UseEffect small summary
- The FiberNdoe node will have another updateQueue linked list to hold all the effects that need to be executed for the render.
- The mountEffect and updateEffect phases mountEffect to updateQueue.
- In the updateEffect phase, an effect that has not changed dePS will be tagged with a NoHookEffect tag, and this effect will be skipped in the Commit phase.
So far, useState useReducer/useEffect source also finished reading, believe that with this basis, the rest of the Hooks source reading won’t be a problem, finally put on full graphic: