Click on the React source debug repository.
React follows the functional programming philosophy in building its user interface, that is, fixed inputs have fixed outputs. Especially after the introduction of functional components, the concept of purely functional components is strengthened. React provides a hook for use(Layout) effects to manage the side effects of requesting data, subscribed to events, and manipulating the DOM manually.
Next, we will start with the data structure of Effect and sort out the overall process of Use (Layout) effect in render and Commit stages.
Effect data structure
Remember that react-hooks are the basic concepts of the react-hooks system. For a function component, the memorizedState on fiber is dedicated to storing a list of hooks, each hook for each element in the list. The hooks generated by use(Layout) effects are placed on Fiber.memorizedState, and they are eventually called to generate an Effect object, which is stored in their own memoizedState of the hook, and connected to the other effects in a circular linked list.
A single Effect object contains the following properties:
- Create: Passes the first argument to the use (Layout) Effect function, which is the callback function
- Destroy: A function that calls back to return and executes when the effect is destroyed
- Deps: dependencies
- Next: points to the next effect
- Tag: indicates the type of effect. It can be useEffect or useLayoutEffect
Simply looking at the fields in an Effect object makes it easy to relate them to ordinary usage. Create is the callback we pass in to use(Layout)Effect. With DEps, we can control whether create executes or not. To destroy Effect, we can return a new function within create (destroy).
To understand the data structure of Effect, assume the following components:
const UseEffectExp = () = > {
const [ text, setText ] = useState('hello')
useEffect(() = > {
console.log('effect1')
return () = > {
console.log('destory1');
}
})
useLayoutEffect(() = > {
console.log('effect2')
return () = > {
console.log('destory2'); }})return <div>effect</div>
}
Copy the code
MemoizedState hooks are mounted to it on Fiber as follows
For example, memoizedState on useEffect Hook stores the Effect object of useEffect (Effect1), and next points to the Effect object of useLayoutEffect (Effect2). Effect2’s next refers back to Effect1. In the useLayoutEffect hook below, the same structure is used.
Fiber. MemoizedState - > useState hook | | next | left useEffect hook memoizedState: UseEffect effect object - > useLayoutEffect effect object | write __________________________________ | | next | left useLayoutffect hook MemoizedState: effect of useLayoutEffect object - > useEffect effect object write ___________________________________ |Copy the code
In addition to being stored in the hooks corresponding to Fiber. MemoizedState, Effect will be stored in Fiber’s Update Ue.
Fiber. UpdateQueue - > useLayoutEffect - next -- -- -- -- > useEffect write | | __________________________ |Copy the code
Now we know that calling Use (Layout)Effect results in an Effect linked list, which is stored in two places:
- In memoizedState, use(Layout)Effect corresponds to memoizedState of the hook element.
- Fiber. Update Ue, the update ue for this update, will be processed during the commit phase of this update.
Description of the process
Based on the data structure above, React does exactly what a Use (Layout) Effect does
- Render phase: when the function component starts rendering, create the corresponding hook list and mount it to workInProgress’s memoizedState, and create the Effect list, but based on the result of comparing last dependency with this dependency,
The effects created are different. For the moment, this can be interpreted as: if a dependency changes, an effect can be processed, otherwise it will not be processed.
- Commit phase: Asynchronous scheduling useEffect; Layout phase: synchronous processing useLayoutEffect effect. Once the COMMIT phase is complete and the update is applied to the page, the effects generated by useEffect are processed.
The second point mentions an important point, that is, useEffect and useLayoutEffect have different execution timing. The former is scheduled asynchronously and executed after the page rendering is completed without blocking the page rendering. The latter is executed synchronously during the COMMIT phase, when the new DOM is ready to be completed, but before rendering to the screen.
Implementation details
As can be seen from the overall process, the whole process of effect involves render stage and Commit stage. The Render phase only creates the effect list, and the Commit phase processes the list. All the implementation details are around the effect linked list.
Render stage – Create effect linked list
In the actual use, we call the use(Layout)Effect function, in the process of mounting and updating is different.
When mounted, we call mountEffectImpl, which creates a hook object for hooks like Use (Layout)Effect, points the workInProgressHook to it, and then adds a side-effectTag to the Fiber flag. Finally, an effect linked list is built and mounted to fiber’s Update Ue, and memorizedState is also mounted on the hook.
function mountEffectImpl(fiberFlags, hookFlags, create, deps) :void {
// Create a hook object
const hook = mountWorkInProgressHook();
// Get dependencies
const nextDeps = deps === undefined ? null : deps;
// Add side effectTag for fiber
currentlyRenderingFiber.flags |= fiberFlags;
// Create an effect linked list and mount it to Hook's memoizedState and Fiber's Update Ue
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
Copy the code
CurrentlyRenderingFiber is the workInProgress node
When you update, call updateEffectImpl to complete the construction of the Effect linked list. This process creates different Effect objects depending on whether the dependencies change. This is reflected in the tag of effect. If the dependency remains unchanged, the tag of effect is assigned to the hookFlags passed in; otherwise, the HookHasEffect flag bit is added to the tag. Because of this, only dependent changes can be handled when dealing with an effect linked list, and a use(Layout) effect can be used to determine whether or not to perform a callback based on its dependent changes.
function updateEffectImpl(fiberFlags, hookFlags, create, deps) :void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if(currentHook ! = =null) {
// Get the last effect from currentHook
const prevEffect = currentHook.memoizedState;
// Get the destory function of the previous effect, the useEffect callback that returns it
destroy = prevEffect.destroy;
if(nextDeps ! = =null) {
const prevDeps = prevEffect.deps;
// Push an effect without HookHasEffect
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
// If the dependency changes, add HookHasEffect to the effect tag
// Update the new effect to hook.memoizedState
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
Copy the code
The difference between mounting and updating components is that the destroy function is not passed when pushEffect is called to create an effect object during mount, but is passed during update. This is because each effect execution takes place after the previous destruction function. During the mount, the previous effect does not exist and does not need to be destroyed before the function is created.
PushEffect is called for both mounting and updating, and its job is simply to create an effect object, build an Effect linked list, and attach it to the WIP node’s updateQueue.
function pushEffect(tag, create, destroy, deps) {
// Create an effect object
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
// Get updateQueue from the workInProgress node in preparation for building the linked list
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
// If updateQueue is empty, place effect in the linked list and close a loop with itself
componentUpdateQueue = createFunctionComponentUpdateQueue();
// Assign the updateQueue value to the WIP node updateQueue to mount the effect list
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// updateQueue is not empty, attach effect to the end of the linked list
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
The update value of both a function component and a class component is a circular list
This is the process of building an effect linked list. As you can see, the effect object is eventually created and placed in two places in two forms: a single effect on a hook. MemorizedState; The circular list of effects is placed in the Update Ue of the Fiber node. The effect of the former will be used as the last update to provide a reference for the creation of the effect object (contrast dependency array), while the effect linked list of the latter will be the subject of the final execution and processed in the COMMIT phase.
How is commit stage-effect handled
UseEffect and useLayoutEffect are all processed in fiber. Update Ue. In the case of the former, only effects containing useEffect tags are processed while in the case of the latter, Only effects that contain tags such as useLayoutEffect are processed by executing the destroy function of the previous update followed by the create function of the new effect.
The above is their processing process in micro common, macro differences mainly reflected in the implementation of the timing. UseEffect is scheduled asynchronously in the beforeMutation or Layout phase and then executed after the update is applied to the screen, while useLayoutEffect is executed synchronously in the Layout phase. Next, the processing process of useEffect is analyzed.
UseEffect asynchronous scheduling
Unlike componentDidMount and componentDidUpdate, the function passed to useEffect is delayed after the browser has laid out and drawn.
This makes it suitable for many common side effect scenarios, such as setting up subscriptions and event handling, so blocking the browser update screen should not be performed in the function.
Based on the requirement of useEffect callback delay (actually asynchronous call), scheduler’s asynchronous scheduler function: scheduleCallback is implemented to schedule useEffect execution as a task, which is called asynchronously.
Commit phase and useEffect are really related in three places: the start of commit phase, beforeMutation and layout, and asynchronous scheduling is involved in the latter two.
function commitRootImpl(root, renderPriorityLevel) {
UseEffect is executed once before the commit phase
do {
flushPassiveEffects();
} while(rootWithPendingPassiveEffects ! = =null); .do {
try {
/ / beforeMutation phase processing function: commitBeforeMutationEffects inside,
useEffect
commitBeforeMutationEffects();
} catch(error) { ... }}while(nextEffect ! = =null); .const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
// Key, record the effect with side effectsrootWithPendingPassiveEffects = root; }}Copy the code
What is the purpose of these three places to perform or schedule useeffects? Let’s look at them separately.
-
UseEffect: This is due to the asynchronous scheduling nature of useEffect. It is scheduled with a normal priority, which means that once a higher priority task enters the COMMIT phase, the last task’s useEffect has not been executed. Therefore, all previous Useeffects need to be executed before this update to ensure that all useeffects scheduled this time are generated by this update.
-
BeforeMutation phase asynchronous scheduling useEffect: Deselect useEffect asynchronously for effectList nodes that have side effects.
function commitBeforeMutationEffects() {
while(nextEffect ! = =null) {...if((flags & Passive) ! == NoFlags) {// If the flags on the Fiber node have Passive dispatch useEffect
if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
scheduleCallback(NormalSchedulerPriority, () = > {
flushPassiveEffects();
return null; }); } } nextEffect = nextEffect.nextEffect; }}Copy the code
Because of the limitation of rootDoesHavePassiveEffects, will only launch a useEffect scheduling, the equivalent of a lock lock scheduling condition, to avoid a schedule for many times.
- The Layout stage populates the effect execution array: the actual useEffect execution actually destroys the previous effect and then creates the current effect. React uses two arrays to store the destruction function and
Create a function that fills the two arrays in the Layout stage, and then loop out and execute the functions in both arrays.
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
) :void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
...
The Layout stage populates the Effect execution array
schedulePassiveEffects(finishedWork);
return; }}Copy the code
When schedulePassiveEffects is called to populate the effect execution array, it is important to place effect in the array only if it contains the HasEffect effectTag. This ensures that the dependency changes before processing effect. If the dependency is unchanged, the effect tag is assigned to the incoming hookFlags, otherwise, the HookHasEffect bit is added to the tag. Because of this, only dependent effects can be processed when dealing with an effect linked list, and use(Layout) effects can be used to determine whether or not to perform a callback based on their dependent changes.
Implementation of schedulePassiveEffects:
function schedulePassiveEffects(finishedWork: Fiber) {
// Get the updateQueue of the function component
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
// Get the effect list
constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
if(lastEffect ! = =null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
// loop effect linked list
do {
const {next, tag} = effect;
if( (tag & HookPassive) ! == NoHookEffect && (tag & HookHasEffect) ! == NoHookEffect ) {// Push effect into the array when the tag contains HookPassive and HookHasEffect
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
Copy the code
In the call enqueuePendingPassiveHookEffectUnmount and enqueuePendingPassiveHookEffectMount filling arrays, also asynchronous scheduling useEffect once again, But this scheduling is incompatible with beforeMutation, once the schedule before, don’t dispatch, is also a rootDoesHavePassiveEffects role.
Execution effect
At this point we already know that effect is processed because of the previous schedule and the effect array population. Now for the final step, effect destroy and Create. The process is to loop through the effect array to be destroyed and then loop through the effect array to be created. This happens in the flushPassiveEffectsImpl function. Each of the two items in the loop are removed because the odd number of items store the current fiber.
function flushPassiveEffectsImpl() {
// If there is no Passive efectTag on the root node, return
if (rootWithPendingPassiveEffects === null) {
return false; }...// Execute effect destruction
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
try {
destroy();
} catch(error) { captureCommitPhaseError(fiber, error); }}}// Execute effect creation again
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
try {
const create = effect.create;
effect.destroy = create();
} catch(error) { captureCommitPhaseError(fiber, error); }}...return true;
}
Copy the code
UseLayoutEffect synchronization execution
When useLayoutEffect is executed, it is destroyed first and then created. Different from useEffect, both are executed synchronously. The former is executed in the mutation phase, while the latter is executed in the layout phase. Unlike useEffect, it does not use arrays to store destruction and create functions. Instead, it operates directly on Fiber.updatequeue.
The last uninstall effect occurred in the mutation stage
/ / call uninstall layout effect function, to layout the effectTag and effect change effectTag: HookLayout | HookHasEffect
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
/ / get updateQueue
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
// Loop through the Effect list on update Ue
if(lastEffect ! = =null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Perform the destruction
const destroy = effect.destroy;
effect.destroy = undefined;
if(destroy ! = =undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
Copy the code
The effect creation takes place in the Layout phase
// Call the function that creates the Layout effect
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
if(lastEffect ! = =null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
/ / create
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
Copy the code
conclusion
UseEffect and useLayoutEffect, as component side effects, are essentially the same. Share a set of structures to store the effect linked list. In the overall process, effects are generated in the Render stage, spliced into linked lists, stored in fiber. Update Ue, and finally processed in the Commit stage. The only difference between them is the final execution timing, one asynchronous and one synchronous, which makes useEffect not block rendering whereas useLayoutEffect does.