Update the React
1. Three update modes
ReactDOM.render || hydrate
setState
forceUpdate
The react-dom/client file contains the following files
2. Data structure of fiberRoot object
1. FiberRoot The starting point of the entire application contains information about the mounted target node that records the entire application update process
//ReactFiberRoot.js
export function createFiberRoot(
containerInfo: any,/ /! Dom node of a real Container
isConcurrent: boolean,
hydrate: boolean,
) :FiberRoot {
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(isConcurrent);/ /! It's a fiber object because the root node can also update this fiber is the node of the whole fiber tree
let root;
if (enableSchedulerTracing) {
root = ({
current: uninitializedFiber,/ /! Fiber object corresponding to the root node
containerInfo: containerInfo,/ /! Reactdom. render The real DOM node passed in
pendingChildren: null./ /! Do not use persistent updates
/ /! The following time is the time marked when the task is scheduled
earliestPendingTime: NoWork,
latestPendingTime: NoWork,
earliestSuspendedTime: NoWork,
latestSuspendedTime: NoWork,
latestPingedTime: NoWork,
/ /! Flag whether errors occur during application rendering
didError: false./ /! A ExpirationTime for a task that is waiting to be submitted
pendingCommitExpirationTime: NoWork,
/ /! If only one fiberRoot object has been completed, it can only be fiber or NULL corresponding to that Root
finishedWork: null./ /! Suspense returns content for setTimeout Settings when tasks are suspended
timeoutHandle: noTimeout,
/ /! Not how to use
context: null.pendingContext: null./ /! Whether dom nodes need to be merged
hydrate,
/ /! So when we're rendering this update, we're going to iterate over every expirationTime of every node and then this is the highest expirationTime of the root. If you would like to iterate over a expirationTime of a node, then you would not update this node
nextExpirationTimeToWorkOn: NoWork,
/ /! The scheduled time is updated with the corresponding expiration time
expirationTime: NoWork,
/ /! useless
firstBatch: null./ /! Creating a one-way linked list structure can have multiple FiberRoots
nextScheduledRoot: null.interactionThreadID: unstable_getThreadID(),
memoizedInteractions: new Set(),
pendingInteractionMap: new Map(),
}: FiberRoot);
} else {
root = ({
current: uninitializedFiber,
containerInfo: containerInfo,
pendingChildren: null.earliestPendingTime: NoWork,
latestPendingTime: NoWork,
earliestSuspendedTime: NoWork,
latestSuspendedTime: NoWork,
latestPingedTime: NoWork,
didError: false.pendingCommitExpirationTime: NoWork,
finishedWork: null.timeoutHandle: noTimeout,
context: null.pendingContext: null,
hydrate,
nextExpirationTimeToWorkOn: NoWork,
expirationTime: NoWork,
firstBatch: null.nextScheduledRoot: null,
}: BaseFiberRootProperties);
}
Copy the code
3. Fiber object data structure
Each reactElement corresponds to a Fiber object, which is used to record various node states, such as props. Fiber will be updated to the props and state of this. ClassComponent. Fiber is also connected to the entire application
//ReactFiber.js
export type Fiber = {|
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
// Tag identifying the type of fiber.
tag: WorkTag,/ /! Different component types have native class Component and function Component with different properties and update methods
// Unique identifier of this child.
key: null | string,/ /! The React. The key element
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,/ /! It's react.element. type so there's context, refProvider, etc, $$type in there
// The resolved function/class/ associated with this fiber.
type: any,/ /! Log whether the asynchronous component reslove is a function component or a class component (lazy component)
// The local state associated with this fiber.
/ /! An instance of a node such as a class component is null and a DOM node is a DOM node
/ /! Once we have this property, we can put the updated data of our node property onto this object
stateNode: any,
// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
// Remaining fields belong to Fiber
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
/ /! The following three properties are used to build the Fiber tree
return: Fiber | null./ /! Point to the parent node
// Singly Linked List Tree Structure.
child: Fiber | null./ /! Point to child node
sibling: Fiber | null./ /! Point to sibling nodes
index: number,
// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref: null | (((handle: mixed) = > void) & {_stringRef: ?string}) | RefObject,/ /! The ref that comes in
// Input is the data coming into process this fiber. Arguments. Props.
/ /! An application update like setState is going to generate a new props that's going to store that property
pendingProps: any, // This type will be more specific once we overload the tag.
/ /! The old props was last updated
memoizedProps: any, // The props used to create the output.
// A queue of state updates and callbacks.
/ /! The update queue records all updates that have been updated
updateQueue: UpdateQueue<any> | null.// The state used to create the output
/ /! Update the old state before
memoizedState: any,
// A linked-list of contexts that this fiber depends on
/ /! The context related
firstContextDependency: ContextDependency<mixed> | null.// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
/ /! ConcurrentMode and strictMode are equal to the mode of the parent node
mode: TypeOfMode,
/ /! Record side effects
// Effect
effectTag: SideEffectTag,
// Singly linked list fast path to the next fiber with side-effects.
nextEffect: Fiber | null.// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
firstEffect: Fiber | null.lastEffect: Fiber | null.// Represents a time in the future by which this work should be completed.
// Does not include work found in its subtree.
/ /! Expiration time of node update
expirationTime: ExpirationTime,
/ /! The update expiration time of the child node
// This is used to quickly determine if a subtree has no pending changes.
childExpirationTime: ExpirationTime,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
/ /! <==>woringProgress current is the old fiber object and woringProgress is the new fiber object. current.alternate = working-fiber working-fiber.alternate = current
alternate: Fiber | null./ /! Node rendering time is related
// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.actualDuration? : number,// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.actualStartTime? : number,// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.selfBaseDuration? : number,// Sum of base times for all descedents of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.treeBaseDuration? : number,// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only_debugID? : number, _debugSource? : Source |null, _debugOwner? : Fiber |null, _debugIsCurrentlyTiming? : boolean, |};Copy the code
Fiber figure
Rounding out the alternate
For the first rendering, the rendered fiber node tree is called current Tree. When the rendering is updated, the fiber node will be reused, the current tree will remain unchanged, and the update content will be reflected as another Fiber node tree work-in-Progress tree. When all rendering processes are complete, the work-in-Progress tree is refreshed to the page and becomes a new current tree (current tree represents rendered content; Work-in-progress tree represents render content in process). Fiber nodes in the current tree and work-in-Progress tree hold each other with alternate properties, so that the processing mode of pairing is convenient to delay batch application of updates.
Summary: There are two fibers in the update process, one is fiber1(Current) before the update and the other is Fiber2 (working-Fiber) after the update.
Alternate = Fiber1. Alternate = Fiber2. Alternate = Fiber1 then current becomes working-fiber for subsequent update.
4.update
The function that records component state changes is stored in the Fiber object’s updateQueue. Multiple updates can exist simultaneously, such as setState() three times in a row. We’ll figure out the final state and only update it once
//react-dom/ReactUpdateQueue.js
//update
{
/ /! ExpirationTime expirationTime of this update
expirationTime: expirationTime,
/ /! 0= update state 1= replace state 2= force update 3= catch render error and update to the UI we need to return when we catch render error
tag: UpdateState,
/ /! The actual content of the update operation, if it's the first time it's passed in the children of the Reactlist and if it's setState the payload is passed in as a callback for state
payload: null.callback: null.next: null./ /! The next update update ue is a one-way linked list structure
nextEffect: null./ /! Next side effect
};
//updateQueue
export type UpdateQueue<State> = {
/ /! Count all the updates in the queue and then calculate a new state that will be updated next time
baseState: State,
/ /! The first update in the queue
firstUpdate: Update<State> | null.lastUpdate: Update<State> | null./ /! The first update to catch the wrong type
firstCapturedUpdate: Update<State> | null.lastCapturedUpdate: Update<State> | null.firstEffect: Update<State> | null.lastEffect: Update<State> | null.firstCapturedEffect: Update<State> | null.lastCapturedEffect: Update<State> | null};//enqueueUpdate
Copy the code
5. ExpirationTime calculation
//ReactFiberScheduler.js
Copy the code
/ /! A high-priority expirationTime has two different constants
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// This is an async update
/ /! A low-priority expirationTime has two different constants
expirationTime = computeAsyncExpiration(currentTime);
Copy the code
**React has two types of ExpirationTime. For example, if the response is triggered by an event, its priority will be higher because of the interaction involved. ** So asynchronous tasks are low and can be interrupted
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
// The low priorities are 5000 and 250
export function computeAsyncExpiration(
currentTime: ExpirationTime,
) :ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
// The high priority is 500 100
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
function ceiling(num: number, precision: number) :number {
return (((num / precision) | 0) + 1) * precision;
}
function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs,) :ExpirationTime {
return (
MAGIC_NUMBER_OFFSET +
ceiling(
currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
// You would like to see an ExpirationTime
export function msToExpirationTime(ms: number) :ExpirationTime {
// Always add an offset so that we don't clash with the magic number for NoWork.
return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
}
// You would like to see an ExpirationTime
export function expirationTimeToMs(expirationTime: ExpirationTime) :number {
return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE;
}
Copy the code
The final formula is the two numbers in bold that represent the change
((((currentTime – 2 + 5000 / 10) / 25) | 0) + 1) * 25
Minus 2 is just because msToExpirationTime adds a 2 to both of them
The key is there is also a step integer | 0 means integer
That is, your currentTime changes within 250ms and this value stays the same 250 corresponds to a Batch size
This is 25 because = LOW_PRIORITY_BATCH_SIZE(250)/ UNIT_SIZE(10) but why is currentTime within 250 because you go to MS and currentTime has removed UNIT_SIZE(10) So these two times are equivalent to ms÷250
This is probably done so that two very similar updates (setState) get the same expirationTime, with the same priority, and then complete in one update, equivalent to an automatic batch update
6. The meaning of expirationTime
We know that there are two types of expirationTime and the first type is a context event and the second type is an asynchronous task but if you add an asynchronous task that keeps being interrupted by some high-priority task and you wait longer than your expirationTime then that task will be forced So a high-priority task would have a smaller expirationTime, which would force it to wait less
Data structure relationship combing
Fiber has a context update that contains updates. Update contains context Time and payload.
7. Different expirationTime
1. Sync Synchronization mode: The system is created when the priority is the highest
2. Asynchronous mode: Task scheduling according to the expirationTime task scheduling is mainly divided into two types of trigger events and ordinary asynchronous tasks (trigger events have a higher priority than ordinary)
3. Specify the context with the highest priority
Context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context = context
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
let expirationTime;
/ /! ExpirationContext is just a expirationTime that starts with nowork=0
/ /! Context = context = context = context = context = context = context = context = context = context = context Context=sync = expirationContext=sync = {context = expirationContext=sync = {context = expirationContext=sync = {context = expirationContext=sync}
if(expirationContext ! == NoWork) {// An explicit expiration context was set;
/ /! ExpirationContext = computeAsyncExpiration(currentTime); expirationContext = computeAsyncExpiration(currentTime)
/ /! ExpirationContext = Sync if you want to expirationContext = Sync if you want to expirationTime=expirationContext Sync makes the callback task the highest priority
expirationTime = expirationContext;
} else if (isWorking) {
/ /! There are tasks that are being updated and there are external forces that specify expirationTime
if (isCommitting) {
// Updates that occur during the commit phase should have sync priority
// by default.
expirationTime = Sync;
} else {
// Updates during the render phase should expire at the same time as
// the work that is being rendered.expirationTime = nextRenderExpirationTime; }}else {
/ /! In the absence of external coercion
// No explicit expiration context was set, and we're not currently
// performing work. Calculate a new expiration time.
/ /! If the Mode is in ConcurrentMode, you will calculate the expirationTime
if (fiber.mode & ConcurrentMode) {
if (isBatchingInteractiveUpdates) {/ /! This value is true for callback functions that use high/low priority for most events
// This is an interactive update
/ /! A high-priority expirationTime has a constant different event triggers tasks of a higher priority
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// This is an async update
/ /! A low priority expirationTime has a constant different from a normal asynchronous task with a low priority
expirationTime = computeAsyncExpiration(currentTime);
}
// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
if(nextRoot ! = =null && expirationTime === nextRenderExpirationTime) {
expirationTime += 1;/ /! To distinguish between this update and the next}}else {
/ /! Set to sync instead of ConcurrentMode to synchronize updates
// This is a sync updateexpirationTime = Sync; }}if (isBatchingInteractiveUpdates) {
// This is an interactive update. Keep track of the lowest pending
// interactive expiration time. This allows us to synchronously flush
// all interactive updates when needed.
if(expirationTime > lowestPriorityPendingInteractiveExpirationTime) { lowestPriorityPendingInteractiveExpirationTime = expirationTime; }}return expirationTime;
}
Copy the code
React fiber mode binary represents different values of 1. The advantage is that we can use and or to quickly determine which modes are in the fiber binary
/ /! The react Fiber type binary has the advantage of using and and to quickly determine which modes are present
export type TypeOfMode = number;
export const NoContext = 0b000;//0b stands for binary
export const ConcurrentMode = 0b001;
export const StrictMode = 0b010;
export const ProfileMode = 0b100;
/ /! Binary explanationFor example, a =000 b=001 c=010 d=100
letMode = a mode&a returns0It means that no mode a returns1Has this pattern mode = mode | b this operation said the b this pattern to join in Mode = at this time001Mode&b return1Mode = mode mode = | c at this time011 mode&c=1
Copy the code
8. SetState & forceUpdate to update components
Fiber for nodes to create updates but their update types are different with different tags
/ /! The core logic of the React class component
const classComponentUpdater = {
isMounted,
/ /! The payload is the setState callback function
enqueueSetState(inst, payload, callback) {
/ /! This ReactInstanceMap is the mapping between Fiber and INST
const fiber = ReactInstanceMap.get(inst);
/ /! Calculate currentTime and expirationTime and Update
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.payload = payload;/ /! The setState update function of the class component is the callback to updateContainer. Payload is the child child
if(callback ! = =undefined&& callback ! = =null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;/ /! Set callback to update if there is a callback
}
enqueueUpdate(fiber, update);/ /! Join the queue
scheduleWork(fiber, expirationTime);/ /! Start Task Scheduling
},
enqueueReplaceState(inst, payload, callback) {
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.tag = ReplaceState;
update.payload = payload;
if(callback ! = =undefined&& callback ! = =null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'replaceState');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueForceUpdate(inst, callback) {
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.tag = ForceUpdate;/ /! The only difference is that the tag is ForceUpdate
if(callback ! = =undefined&& callback ! = =null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate'); } update.callback = callback; } enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }};Copy the code
The flow chart
Q: We calculate expirationTime passed in fiber and currentTime so the expirationTime calculated is one-to-one with fiber, but a fiber a component has different tasks inside how to distinguish priority.