It has been a year and a half since I switched to React. After familiar with its usage, can not avoid to in-depth understanding of its implementation principle, the Internet related source analysis of the article is quite a lot, but the total feeling is not as profound as their reading understanding. So I spent a few weekends to understand the common process. It is also through this article that I share my personal understanding.
Before the specific source process analysis, according to personal understanding, combined with better online articles, first to analyze some conceptual things. Then analyze the specific process logic.
React 15
Architectural layering
React 15 (before Fiber) The entire rendering process is divided into two parts:
Reconciler
(coordinator); Responsible for identifying changing componentsRenderer
(renderer); Responsible for rendering the changing components onto the page
Reconciler
React can trigger updates with setState, forceUpdate, and reactdom.render. Every time a renewal occurs, the Reconciler does the following:
- Calling component
render
Method that will be returnedJSX
Convert to virtualDOM
- The virtual
DOM
And virtual from the last updateDOM
contrast - Find out the virtual changes in this update by comparison
DOM
- notice
Renderer
Render the changing virtual DOM onto the page
Renderer
After the Reconciler is played on an updated node, the Renderer is notified to render/update the node according to the different “host environment.”
React 15’s flaws
The React 15 diff process performs updates recursively. Because it’s recursive, once it starts, it’s “unstoppable.” When the hierarchy is too deep or the diff logic (the logic in the hook function) is too complex, causing the recursive update to take too long and the Js thread to stay stuck, the user interaction and rendering can get stuck. Look at an example: count-demo
<button> click <button>
<li>1<li> -> <li>2<li>
<li>2<li> -> <li>4<li>
<li>3<li> -> <li>6<li>
Copy the code
When the button is clicked, the list changes from 1, 2 and 3 on the left to 2, 4 and 6 on the right. Updates on each node are basically synchronous to the user, but they are actually traversed sequentially. The specific steps are as follows:
- Click on the
button
, trigger the update Reconciler
detected<li1>
Need to change to<li2>
, immediately notifyRenderer
updateDOM
. List theTwo, two, three
Reconciler
detected<li2>
Need to change to<li4>
To informRenderer
updateDOM
. List theTwo, four, three
Reconciler
detected<li3>
Need to change to<li6>
, immediately notifyRenderer
updateDOM
. List theTwo, four, six
It is then clear that the Reconciler and the Renderer work alternately, with the second node entering the Reconciler after the first node has changed on the page. Because the entire process is synchronous, all nodes are updated simultaneously from the user’s point of view. If you break the update, you will see a new node tree on the page that is not fully updated!
If step 3 and step 4 cannot proceed due to a sudden interruption of the current task during step 2, the user will see:
<button> click <button>
<li>1<li> -> <li>2<li>
<li>2<li> -> <li>2<li>
<li>3<li> -> <li>3<li>
Copy the code
React definitely doesn’t want this to happen. But this application scenario is absolutely necessary. Imagine a user making an input event at a point in time when the content in the input should be updated, but the user’s input is delayed because of an update to a list that is not currently visible. The experience for the user is sluggish. So the React team needed to find a way to address this flaw.
React 16
Architectural layering
The Act15 architecture could not support asynchronous updates and required refactoring, so the Act16 architecture was split into three layers:
- Scheduler b. Scheduling tasks are prioritized, and high-priority tasks are given priority into Reconciler
- It is a Reconciler; Responsible for identifying changing components
- Renderer; Responsible for rendering the changing components onto the page
Scheduler
React 16 requires Diff updates to be interruptible. React 15 requires Diff updates to be interruptible. React 16 requires Diff updates to be interruptible.
The React team uses cooperative scheduling, which involves active interrupts and controller transfers. The criterion is timeout detection. There is also a need for a mechanism to tell interrupted tasks when to resume/resume. React uses the browser’s requestIdleCallback interface to notify users when the browser has time left.
React abandoned rIdc for some reason, instead implementing its own, more fully functional Polyfill, or Scheduler. In addition to the ability to trigger callbacks when idle, Scheduler provides a variety of scheduling priorities for tasks to set.
Reconciler
In React 15, the Reconciler is dealt with the Virtual DOM recursively. Act16 uses a new data structure: Fiber. The Virtual DOM tree is changed from the previous top-down tree structure to a “graph” based on multi-directional linked lists.
The update process has gone from being recursive to a cycle that can be interrupted. Each time the loop calls shouldYield() to see if there is any time left. Source code address.
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while(workInProgress ! = =null&&! shouldYield()) { workInProgress = performUnitOfWork(workInProgress); }}Copy the code
According to the previous analysis, the interruption of React 15 resulted in incomplete page updates because Reconciler and Renderer work alternately, so in React 16, Reconciler and Renderer no longer work alternately. When Scheduler hands Reconciler the task, the Reconciler simply marks the changed Virtual DOM with add, delete, and update, without notifies the Renderer to render. Something like this:
export const Placement = / * * / 0b0000000000010;
export const Update = / * * / 0b0000000000100;
export const PlacementAndUpdate = / * * / 0b0000000000110;
export const Deletion = / * * / 0b0000000001000;
Copy the code
Only when all components are finished with the Reconciler are the Reconciler uniformly handed over to the Renderer for rendering updates.
Renderer(Commit)
The Renderer synchronously performs the corresponding rendering operations based on the Reconciler’s markings for the Virtual DOM.
For the example we used in the previous section, the entire update process in the React 16 architecture looks like this:
setState
Generates an update with the following contents:state.count
from1
into2
- Update is given
Scheduler
.Scheduler
Find no other higher priority and assign the taskReconciler
Reconciler
Got a call. Start traversingVirtual DOM
And judge which onesVirtual DOM
Need to update, for need to updateVirtual DOM
brandingReconciler
Go through all of themVirtual DOM
To informRenderer
Renderer
According to theVirtual DOM
To perform the corresponding node operation
Steps 2, 3, and 4 May be interrupted at any time due to the following reasons:
- There are other higher priority tasks that need to be updated first
- The current frame has no time left
Because Scheduler and Reconciler work in memory and do not update the nodes on the page, users do not see pages that are not fully updated.
The principle of Diff
React Diff is based on three assumptions:
- DOM is rarely moved across hierarchies, right
Virtual DOM
Trees are compared hierarchically. Two trees are compared only on nodes of the same hierarchy. - The tree structure varies with different types of components. Components of the same type have similar tree structures
- A group of child nodes at the same level can be updated, removed, or added. Nodes can be distinguished by their unique IDS
The React component, either in JSX format or created by React. CreateElement, will eventually be converted to the Virtual DOM, and the corresponding Virtual DOM tree will be generated based on the hierarchy. React 15 creates a new Virtual DOM each time it is updated, and then compares the differences between the old and new Virtual DOM recursively, resulting in a “patch update” that maps to the real DOM. The React 16 process will be analyzed later
Source code analysis
React source code is very large, and the source code has been adjusted since 2016. At present, the latest source code on Github is reserved xxx.new.js and xxx.old.js. React source code is managed by using Monorepo structure. Different functions are grouped into different packages. The only disadvantage may be that method address index is not very convenient. Before you start, check out this official reading guide
Because the source code is too much too complex, all I here as far as possible from the largest to small, from surface to point one analysis. The general process is as follows:
- The first thing to know is to pass
JSX
orcreateElement
What the coded code actually turns into - Then analyze the entry point of the application
ReactDOM.render
- Further analysis
setState
Update process - Finally, specific analysis
Scheduler
,Reconciler
,Renderer
The general process of
In addition to reactdom. render and setState, there are forceUpdate operations that trigger render updates. The main difference is that forceUpdate does not walk shouldComponentUpdate hook function.
The data structure
Fiber
Before starting the formal process analysis, I hope you have a certain understanding of Fiber. If not, I suggest you watch this video first. Then, familiarize yourself with the structure of the ReactFiber.
export type Fiber = {
// Task type information;
// Such as ClassComponent, FunctionComponent, ContextProvider
tag: WorkTag,
key: null | string,
// The value of reactElement. Type, used for the reserved identifier during reconciliation.
elementType: any,
// fiber is associated with function/class
type: any,
// Any type!! This generally refers to the actual DOM node or instance of the corresponding component that Fiber corresponds to
stateNode: any,
// Parent node/parent component
return: Fiber | null.// First child node
child: Fiber | null.// Next sibling node
sibling: Fiber | null.// Change the state, such as delete, move
effectTag: SideEffectTag,
// Used to link old and new trees; Old -> new, new -> old
alternate: Fiber | null.// Development mode
mode: TypeOfMode,
// ...
};
Copy the code
FiberRoot
Each tree or application rendered by reactdom.render initializes a corresponding FiberRoot object as the starting point for the application. Its data structure is ReactFiberRoot.
type BaseFiberRootProperties = {
// The type of root (legacy, batched, concurrent, etc.)
tag: RootTag,
// root, the second argument to reactdom.render ()
containerInfo: any,
// Persistent updates are used. React-dom is an entire application update, so you don't need this
pendingChildren: any,
// The Fiber object corresponding to the root node is currently applied
current: Fiber,
// The expiration time of the current update
finishedExpirationTime: ExpirationTime,
// The FiberRoot object that has completed the task will only process the task corresponding to this value during the COMMIT phase
finishedWork: Fiber | null.// The oldest unexpired time in the tree
firstPendingTime: ExpirationTime,
// Suspends the next known due time in the task
nextKnownPendingLevel: ExpirationTime,
// The latest unexpired time in the tree
lastPingedTime: ExpirationTime,
// The latest expiration time
lastExpiredTime: ExpirationTime,
// ...
};
Copy the code
Fiber type
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Undefined type; It could be class or function
export const HostRoot = 3; / / the root of the tree
export const HostPortal = 4; // a subtree
export const HostComponent = 5; // native node; Depending on the environment, the browser environment is div, etc
export const HostText = 6; // Plain text node
export const Fragment = 7;
Copy the code
model
As of React 16.13.1, there are several built-in development modes:
export type TypeOfMode = number;
/ / normal mode | Legacy mode, synchronous rendering, React15-16 production environment
export const NoMode = 0b0000;
// Strict mode, used to detect the presence of deprecated APIS (which call the render phase lifecycle multiple times), used by the react16-17 development environment
export const StrictMode = 0b0001;
// ConcurrentMode transitional version of the mode
export const BlockingMode = 0b0010;
// Concurrent mode, asynchronous rendering, React17 production environment
export const ConcurrentMode = 0b0100;
// Performance test mode, used to detect where there are performance problems, react16-17 development environment use
export const ProfileMode = 0b1000;
Copy the code
This article examines only the ConcurrentMode mode
JSX and React. The createElement method
Let’s take a look at one of the simplest JSX encoded components. Here’s the code conversion with Babel. Here’s the code
// JSX
class App extends React.Component {
render() {
return <div />
}
}
// babel
var App = /*#__PURE__*/function (_React$Component) {
_inherits(App, _React$Component);
var _super = _createSuper(App);
function App() {
_classCallCheck(this, App);
return _super.apply(this, arguments);
}
_createClass(App, [{
key: "render",
value: function render() {
return /*#__PURE__*/React.createElement("div", null);
}
}]);
return App;
}(React.Component);
Copy the code
The key point is that the Render method actually calls the React. CreateElement method. So we just need to analyze what createElement does. Let’s look at the structure of ReactElement:
let REACT_ELEMENT_TYPE = 0xeac7;
if (typeof Symbol= = ='function' && Symbol.for) {
REACT_ELEMENT_TYPE = Symbol.for('react.element');
}
const ReactElement = function (type, key, ref, props) {
const element = {
// Uniquely identified as React Element to prevent XSS from storing symbols in JSON
?typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
}
return element;
}
Copy the code
A very simple data structure, each attribute is clear at a glance, not one explanation. React.createElement
Preventing XSS attacks
If you are not aware of XSS attacks, you are advised to read this article first how to prevent XSS attacks? . First of all, the components we coded are converted to ReactElement objects. DOM manipulation and generation are generated by Js scripts. Basically eliminated three XSS attacks.
React, however, provides dangerouslySetInnerHTML as an alternative to innerHTML. Let’s say there’s a scenario where the interface gives me JSON data. I need to show it in a div. If intercepted by an attacker, the JSON is replaced with a ReactElement structure. So what happens?
I wrote a demo here. What if I got rid of it? Typeof will find an error. And Symbol can not JSON, so the external is also unable to use dangerouslySetInnerHTML attack. Check the source code here
const hasOwnProperty = Object.prototype.hasOwnProperty;
const RESERVED_PROPS = {
key: true.ref: true.__self: true.__source: true};function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
if(config ! = =null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = ' '+ config.key; }}// Filter the React reserved keywords
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
/ / traverse the children
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// Set the default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) { props[propName] = defaultProps[propName]; }}}return ReactElement(type, key, ref, props);
}
Copy the code
The comments should be clear enough. Generate a ReactElement object based on the parameters, and bind the corresponding props, key, ref, etc.
Render process
Reactdom.render uses the reference here
In general, when writing applications with React, reactdom. render is the first function we fire. So let’s start with the reactdom. render entry function to analyze the render process.
Logical judgments and handling of Hydrate occur frequently in the source code. This is related to SSR combined with client rendering, will not do too much analysis. I will omit the source code
Reactdom. render is actually a reference to the Render method in ReactDOMLegacy, with condensed logic as follows:
export function render(
// React. CreatElement's product
element: React$Element<any>,
container: Container,
callback: ?Function.) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
Copy the code
Actually call is legacyRenderSubtreeIntoContainer method, take a look at this
function legacyRenderSubtreeIntoContainer(parentComponent: ? React$Component<any, any>,// Usually null
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function.) {
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if(! root) {// [Q]: Initializes the container. Empty the nodes in the container and create FiberRoot
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// FiberRoot; Starting point of application
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function () {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// [Q]: Initialization cannot be batch processed, that is, synchronous update
unbatchedUpdates(() = > {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
/ / to omit... Similar to the above, except that there is no need to initialize the container and it can be batched
// [Q] : What? What's the mystery of unbatchedUpdates
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
Copy the code
In this step, the container is emptied of existing nodes, and any asynchronous callback is saved and FiberRoot references are bound for subsequent delivery of the correct root node, according to the FiberRoot documentation. I put two [Q] in the comments for two questions. Let’s take a closer look at these two problems
Initialize the
Look from the naming, legacyCreateRootFromDOMContainer is used to initialize the root node. LegacyCreateRootFromDOMContainer return the results to the container. The _reactRootContainer, while _reactRootContainer look from the code is as the basis of whether they have been initialized, and verify it. If you don’t believe me, open your React app and check the _reactRootContainer property of the container element
function legacyCreateRootFromDOMContainer(container: Container, forceHydrate: boolean,) :RootType {
// omission hydrate...
return createLegacyRoot(container, undefined);
}
export function createLegacyRoot(container: Container, options? : RootOptions,) :RootType {
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}
function ReactDOMBlockingRoot(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
/ /!!!!!! look here
this._internalRoot = createRootImpl(container, tag, options);
}
Copy the code
A series of function calls that return an instance of ReactDOMBlockingRoot. The important point is that the attribute _internalRoot is created through createRootImpl.
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// omission hydrate...
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
// omission hydrate...
return root;
}
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
) :OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
) :FiberRoot {
/ / generated FiberRoot
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// Generate Fiber object for Root
const uninitializedFiber = createHostRootFiber(tag);
// Bind FiberRoot to Fiber
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// Generate an update queue
initializeUpdateQueue(uninitializedFiber);
return root;
}
export function initializeUpdateQueue<State> (fiber: Fiber) :void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
baseQueue: null.shared: {
pending: null,},effects: null}; fiber.updateQueue = queue; }Copy the code
The general logic is to generate a FiberRoot object root. The Fiber object corresponding to root is generated, and the update queue of this Fiber is generated. It is clear when FiberRoot was initialized. Remember that FiberRoot is the starting point of the React application.
unbatchedUpdates
The English comments in the source code indicate that batch processing is not required and should be performed immediately. Its passed argument is a wrapper function that performs updateContainer. But you actually do updateContainer in the else judgment as well. So what’s the secret of unbatchedUpdates?
export function unbatchedUpdates<A.R> (fn: (a: A) => R, a: A) :R {
const prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
return fn(a);
} finally {
/ /!!!!!! look here
executionContext = prevExecutionContext;
if(executionContext === NoContext) { flushSyncCallbackQueue(); }}}export function flushSyncCallbackQueue() {
/ / to omit...
flushSyncCallbackQueueImpl();
}
// Clear the synchronization task queue
function flushSyncCallbackQueueImpl() {
if(! isFlushingSyncQueue && syncQueue ! = =null) {
isFlushingSyncQueue = true;
let i = 0;
try {
const isSync = true;
const queue = syncQueue;
// Clear the queue with the highest priority
runWithPriority(ImmediatePriority, () = > {
for (; i < queue.length; i++) {
let callback = queue[i];
do {
callback = callback(isSync);
} while(callback ! = =null); }}); syncQueue =null;
} catch (error) {
// Remove the wrong task
if(syncQueue ! = =null) {
syncQueue = syncQueue.slice(i + 1);
}
// Resume execution at the next execution unit
Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueue,
);
throw error;
} finally {
isFlushingSyncQueue = false; }}}Copy the code
In the case of unbatchedUpdates, there is an extra logic in finally. The logic is mainly to refresh the synchronization task queue. Think about it. Why? So the synchronization task must be generated during the execution of FN (a). So let’s go ahead and check it out in updateContainer.
updateContainer
Note that the updateContainer is already in the Reconciler process. Follow up:
export function updateContainer(
element: ReactNodeList, // The component to render
container: OpaqueRoot, // OpaqueRoot is FiberRootparentComponent: ? React$Component<any, any>, callback: ?Function.) :ExpirationTimeOpaque {
// Root node Fiber
const current = container.current;
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
// [Q]: Calculate the expiration time of this task
const expirationTime = computeExpirationForFiber(
currentTime,
current,
suspenseConfig,
);
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// Create an update task
const update = createUpdate(eventTime, expirationTime, suspenseConfig);
update.payload = { element };
callback = callback === undefined ? null : callback;
if(callback ! = =null) {
update.callback = callback;
}
// Insert the task into Fiber's update queue
enqueueUpdate(current, update);
// scheduleWork is scheduleUpdateOnFiber
scheduleWork(current, expirationTime);
return expirationTime;
}
Copy the code
EnqueueUpdate = expirationTime = expirationTime = expirationTime = expirationTime Finally, scheduleUpdateOnFiber is used to schedule the task.
ExpirationTime calculation
ExpirationTime is a very important concept. React prevents an update from being interrupted due to priority. React sets a expirationTime. When a expirationTime is expirationTime, if an update has not been executed, React will force that update. That’s what expirationTime does.
This is the first time we have encountered its computational logic. Let’s break it down.
The first step is to calculate currentTime, which is essentially a context time with the current timestamp converted to the built-in ExpirationTime. Take a look at
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
const MAX_SIGNED_31_BIT_INT = 1073741823;
export const Sync = MAX_SIGNED_31_BIT_INT;
export const Batched = Sync - 1;
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = Batched - 1;
export function msToExpirationTime(ms: number) :ExpirationTime {
return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
export function requestCurrentTimeForUpdate() {
/ / to omit...
return msToExpirationTime(now());
}
Copy the code
Take a look at computeExpirationForFiber concrete calculation logic
export function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
suspenseConfig: null | SuspenseConfig,
) :ExpirationTime {
const mode = fiber.mode;
// Synchronous mode
if ((mode & BlockingMode) === NoMode) {
return Sync;
}
// Get the current priority from Scheduler
const priorityLevel = getCurrentPriorityLevel();
if ((mode & ConcurrentMode) === NoMode) {
return priorityLevel === ImmediatePriority ? Sync : Batched;
}
// ...
let expirationTime;
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = Sync;
break;
case UserBlockingPriority:
// Same as computeAsyncExpiration. The difference is that the expirationInMs parameter value is smaller.
// Therefore, the smaller the expirationTime, the higher the priority
expirationTime = computeInteractiveExpiration(currentTime);
break;
case NormalPriority:
case LowPriority: // TODO: Handle LowPriority
// TODO: Rename this to... something better.
expirationTime = computeAsyncExpiration(currentTime);
break;
case IdlePriority:
expirationTime = Idle;
break;
default:
invariant(false.'Expected a valid priority level'); }}export const LOW_PRIORITY_EXPIRATION = 5000;
// This BATCH means that?
export const LOW_PRIORITY_BATCH_SIZE = 250;
export function computeAsyncExpiration(
currentTime: ExpirationTime,
) :ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
function ceiling(num: number, precision: number) :number {
return (((num / precision) | 0) + 1) * precision;
}
function computeExpirationBucket(
currentTime,
expirationInMs, / / 5000
bucketSizeMs, / / 250
) :ExpirationTime {
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
Copy the code
In summary, the calculation formula is as follows:
// current = MAGIC_NUMBER_OFFSET - ((now() / UNIT_SIZE) | 0);
// expirationTime = MAGIC_NUMBER_OFFSET - ((((MAGIC_NUMBER_OFFSET - currentTime + 500) / 25) | 0) + 1) * 25
// => MAGIC_NUMBER_OFFSET - ((((((now() / UNIT_SIZE) | 0) + 500) / 25) | 0) + 1) * 25
Copy the code
The | 0 is used for integer. Notice the + 1 operation, what does that say? The difference between two different expirationTime is a multiple of 25, that is, within 25ms tasks are the same expirationTime. So updates for 25ms will be combined into a single task.
As described on the website, Legacy mode has automatic batching in compositing events, but is limited to one browser task. To use this function, non-React events must use unstable_batchedUpdates. In blocking and concurrent modes, all setStates are batch by default. Here are two examples to help you understand:
- Non-concurrent mode setState
- Model of concurrent setState
After analyzing the calculation of expirationTime, continue to look at the logic of scheduleUpdateOnFiber.
From here on, there are synchronous and asynchronous processing methods in the source code, synchronous tasks are not scheduled by Scheduer. For the integrity of the analysis, we will examine only asynchronous processes. ExpirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime But it means different things at different stages. To be sure, it determines whether the component is updated or not, or when it is updated.
export function scheduleUpdateOnFiber(fiber: Fiber, expirationTime: ExpirationTimeOpaque,) {
/ / get FiberRoot
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
return null;
}
if (expirationTime === Sync) {
// Synchronize task scheduling
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
/ / to omit...
}
Copy the code
ScheduleUpdateOnFiber is simply used to update the expiration time of the entire “tree” Root of the current node. One priority is the ensureRootIsScheduled method
// This function is used to schedule tasks. A root(Fiber node) can only have one task running
// If a task is already scheduled, the expiration time of the existing task is the same as that of the next task.
// This function is called every update and before the task exits
// Note: root is FiberRoot
function ensureRootIsScheduled(root: FiberRoot) {
// lastExpiredTime indicates the expiration time
const lastExpiredTime = root.lastExpiredTime;
if(lastExpiredTime ! == NoWork) {// Special case: Expired work should be refreshed synchronously
root.callbackExpirationTime = Sync;
root.callbackPriority = ImmediatePriority;
root.callbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
return;
}
// The next nearest due date
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
// Root has scheduling tasks in progress
const existingCallbackNode = root.callbackNode;
if (expirationTime === NoWork) {
if(existingCallbackNode ! = =null) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
}
return;
}
// Get the expiration time of the current task; All updates of the same priority that occur in the same event receive the same expiration time
const currentTime = requestCurrentTimeForUpdate();
// Calculate the priority of the current task based on the expiration time of the next scheduled task and the expiration time of the current task
// If currentTime is smaller than expirationTime, it has a higher priority
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
expirationTime,
);
// If the priority of the task being processed is based on this task, cancel the task being processed!
if(existingCallbackNode ! = =null) {
const existingCallbackPriority = root.callbackPriority;
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (
// Tasks must have exactly the same due time.
existingCallbackExpirationTime === expirationTime &&
// Compare the priorities of the two tasks
existingCallbackPriority >= priorityLevel
) {
return;
}
// Cancel the task
cancelCallback(existingCallbackNode);
}
// Update expiration time and priority
root.callbackExpirationTime = expirationTime;
root.callbackPriority = priorityLevel;
let callbackNode;
if (expirationTime === Sync) {
/ / to omit...
/ / tasks will be pushed into the synchronization task queue here, in front of the analysis to the flushSyncCallbackQueueImpl empty task is to push from here
} else {
// Push tasks to the Scheduler scheduling queue
callbackNode = scheduleCallback(
priorityLevel,
/ / binding
performConcurrentWorkOnRoot.bind(null, root),
// Calculate the timeout
{ timeout: expirationTimeToMs(expirationTime) - now() },
);
}
// Update Fiber's current callback point
root.callbackNode = callbackNode;
}
Copy the code
The primary logic in ensureRootIsScheduled has three steps:
- Calculate the expiration time and priority of this task.
- If a task is being scheduled on the node. If the expiration time is the same and the priority of existing tasks is higher, the scheduling task is cancelled. Otherwise, the existing task is cancelled.
- Push the task in
Scheduler
And set its priority and task expiration time
Each section of this code can be extended for analysis. But I’m focusing on the general flow here, so I’m focusing on schedulecallback-related logic. For other parts, I will have time for further analysis later.
The scheduleCallback assigns a task’s execution function to the Scheduler. The subsequent process needs to wait for the Scheduler to trigger specific performConcurrentWorkOnRoot executive function. The render process on the first temporary analysis so far.
Summary of Render process
render
Will be calledlegacyRenderSubtreeIntoContainer
methodslegacyRenderSubtreeIntoContainer
If it is the first rendering, it will be initialized firstFiberRoot
, which is the starting point of application. Generate the root node at the same timeFiber
Instance. hereFiberRoot.current
=Fiber
;Fiber.stateNode
=FiberRoot
.- call
updateContainer
The expiration time of this update is calculated. And generate task objectsupdate
, insert itFiber
Update queue, and then callscheduleUpdateOnFiber
Triggering task scheduling scheduleUpdateOnFiber
The expiration time of the entire Fiber tree with the Fiber node as the root node is updated. And then callensureRootIsScheduled
scheduleensureRootIsScheduled
To bind tasks to specific execution functions. And then passed on toScheduler
To deal with
SetState process
Before moving on to the subsequent Reconciler and Renderer details, let’s familiarize ourselves with the setState process while the iron is hot. Since this. SetState is invoked, look for it in Component. Look at the act baseclasses
const emptyObject = {};
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
// ReactNoopUpdateQueue is a meaningless empty object
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Copy the code
The initial structure of Component is simple. We see the setState method is invoked in this. Updater. EnqueueSetState method, but the update is the default empty useless objects, we haven’t passed in the constructor usually an update parameter, it shows this method is certainly subsequent injection. And I looked and looked and found a similar thing classComponentUpdater
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
// Generate the update object for this setState
const update = createUpdate(expirationTime, suspenseConfig);
update.payload = payload;
if(callback ! = =undefined&& callback ! = =null) {
update.callback = callback;
}
// Update quests to join the team
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueReplaceState(inst, payload, callback) {
// Same as above
},
enqueueForceUpdate(inst, callback) {
// Same as above}};Copy the code
The logic in enqueueSetState is a bit familiar. The render process is the same as the updateContainer process. Check back if you don’t remember. The update property of a classComponentUpdater is injected into the Component.
In the previous analysis of the Render process, we only analyzed the generation of task fragments and push into the scheduling queue, and did not analyze the initialization of components. Does the React constructor inject the Component when initializing it? Follow this train of thought for the next step of analysis. First of all, let’s look at a piece of code in the beginWork method, and the beginWork method will be analyzed in detail later. This is the Fiber object used to create the child component.
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) :Fiber | null {
// Try to reuse the current node
if(current ! = =null) {
/ / to omit...
}
// If you cannot reuse it, update or mount it
switch (workInProgress.tag) {
/ / to omit...
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
/ / to omit...}}Copy the code
The code in beginWork is divided into two parts. Used to handle mount and update logic, respectively. The process we analyze is the first initialization, so the mount process is followed. BeginWork calls different methods based on the tag, so let’s look at the updateClassComponent first
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps,
renderExpirationTime: ExpirationTime,
) {
// omit context handling...
// An instance of the component
const instance = workInProgress.stateNode;
let shouldUpdate;
// Instance null indicates that the component was rendered for the first time
if (instance === null) {
if(current ! = =null) {
// Reset current and WIP dependencies (backup)
current.alternate = null;
workInProgress.alternate = null;
// mark it as a new node
workInProgress.effectTag |= Placement;
}
// Initialize the component instance
constructClassInstance(workInProgress, Component, nextProps);
/ / mount; And invoke the appropriate lifecycle
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
shouldUpdate = true;
} else {
// omit update logic...
}
// TODO: execute render to create subfiber.
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
return nextUnitOfWork;
}
Copy the code
function constructClassInstance(workInProgress: Fiber, ctor: any, props: any,) :any {
let context = emptyContextObject;
// omit context related logic...
const instance = new ctor(props, context);
conststate = (workInProgress.memoizedState = instance.state ! = =null&& instance.state ! = =undefined
? instance.state
: null);
adoptClassInstance(workInProgress, instance);
// omit context related logic...
return instance;
}
Copy the code
function adoptClassInstance(workInProgress: Fiber, instance: any) :void {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
// Bind the instance to Fiber for subsequent updates
setInstance(instance, workInProgress);
}
Copy the code
You can see that when instance is null, the following processes are executed
- And mark the current
effectTag
forPlacement
Is a new node - Initialize to an instance and bind to
Fiber(workInProgress)
And bindupdate
attribute - Finally, mountClassInstance is called to mount the node and invoke the associated lifecycle.
At this point, the subsequent update process is consistent with the render process, do not repeat analysis ~
Scheduler
Scheduler is a rIdc polyfill implemented separately by the React team for task scheduling. The React team’s intention is not limited to just one application scenario, but also to serve more businesses and become a tool for wider application.
Minimum priority queue
Since tasks have different expiration times and priorities, a data structure is needed to manage priority tasks. React data structures with a smaller expirationTime would have a higher priority. React is a minimal priority queue based on the small top heap. Let’s just look at the code. SchedulerMinHeap
type Heap = Array<Node>;
type Node = {|
id: number,
sortIndex: number,
|};
// Insert to the end of the heap
export function push(heap: Heap, node: Node) :void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
// Get the heap top task with the smallest sortIndex/id
export function peek(heap: Heap) :Node | null {
const first = heap[0];
return first === undefined ? null : first;
}
// Delete the heap top task
export function pop(heap: Heap) :Node | null {
const first = heap[0];
if(first ! = =undefined) {
const last = heap.pop();
if(last ! == first) { heap[0] = last;
siftDown(heap, last, 0);
}
return first;
} else {
return null; }}// Keep the small top heap up
function siftUp(heap, node, i) {
let index = i;
while (true) {
// bit operation; The parent node is -> I / 2-1
const parentIndex = (index - 1) > > >1;
const parent = heap[parentIndex];
if(parent ! = =undefined && compare(parent, node) > 0) {
// parent larger, switch places
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
return; }}}// Keep the small top heap down
function siftDown(heap, node, i) {
let index = i;
const length = heap.length;
while (index < length) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// // If the left or right child node is smaller than the target node (parent node), the switch is performed
if(left ! = =undefined && compare(left, node) < 0) {
if(right ! = =undefined && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else{ heap[index] = left; heap[leftIndex] = node; index = leftIndex; }}else if(right ! = =undefined && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
return; }}}function compare(a, b) {
// Compare sort index first, then task id.
// Compare task ID with sort index
const diff = a.sortIndex - b.sortIndex;
returndiff ! = =0 ? diff : a.id - b.id;
}
Copy the code
The implementation is to use an array to simulate the structure of a minimal heap. As can be seen, each task insertion or removal will restore the minimum heap structure, sorting rules are supplemented by sortIndex, taskId. In React, sortIndex corresponds to the expiration time, while taskId corresponds to the increasing task sequence. This will be discussed later.
Enabling Task Scheduling
A task node is generated under ensureRootIsScheduled and the task is pushed into the Scheduler via scheduleCallback. So we first from this task into the team method to step by step analysis
var taskIdCounter = 1;
// Currently, Scheduler's API is unstate_, indicating that it is not a stable version
function unstable_scheduleCallback(priorityLevel, callback, options) {
// It is more accurate to call performance.now() or date.now ()
var currentTime = getCurrentTime();
var startTime;
var timeout;
// Determine the start time based on whether there is a delay
if (typeof options === 'object'&& options ! = =null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
// [Q1]: this parameter is directly used if the timeout configuration is enabled. Otherwise, the value is calculated based on the priority
timeout =
typeof options.timeout === 'number'
? options.timeout
: timeoutForPriorityLevel(priorityLevel);
} else {
timeout = timeoutForPriorityLevel(priorityLevel);
startTime = currentTime;
}
// The expiration time is equal to the start time + the timeout time
var expirationTime = startTime + timeout;
// This is the data structure of a task.
var newTask = {
// Tasks with the same timeout are compared with ids, so it is first come, first served
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1};if (enableProfiling) {
newTask.isQueued = false;
}
// [Q2] : There is a timerQueue and a taskQueue.
if (startTime > currentTime) {
// This is a delayed task.
// This is a deferred task; Options. delay exists
newTask.sortIndex = startTime;
// If the start time is longer than the current time, push it into the timer queue, indicating that this is a wait queue
push(timerQueue, newTask);
// If the task queue is empty, all tasks are delayed, and newTask is the earliest delayed task.
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
// If the timeout process is in progress, cancel first and restart later
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Initiate a timeout processingrequestHostTimeout(handleTimeout, startTime - currentTime); }}else {
newTask.sortIndex = expirationTime;
// Non-deferred tasks are thrown to the task queue
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// If not, start scheduling;
if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true;
// [Q] Enable schedulingrequestHostCallback(flushWork); }}// [A] : Return the reference to this task
return newTask;
}
Copy the code
In this code, you can see the data structure of a scheduled task, and the tasks are sorted according to sortIndex, which is the expirationTime of a task, and id is an increasing sequence. Several issues are noted in the notes, which will be analyzed in detail below
The timeout to calculate
// Execute immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// User behavior is blocked
var USER_BLOCKING_PRIORITY = 250;
// The default expiration time is five seconds
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// It will never expire. MaxSigned31BitInt is v8. 32 is the maximum valid value of the system
var IDLE_PRIORITY = maxSigned31BitInt;
function timeoutForPriorityLevel(priorityLevel) {
switch (priorityLevel) {
case ImmediatePriority:
return IMMEDIATE_PRIORITY_TIMEOUT;
case UserBlockingPriority:
return USER_BLOCKING_PRIORITY;
case IdlePriority:
return IDLE_PRIORITY;
case LowPriority:
return LOW_PRIORITY_TIMEOUT;
case NormalPriority:
default:
returnNORMAL_PRIORITY_TIMEOUT; }}Copy the code
As you can see, the priority is converted to the constant time. The higher the priority, the lower the timeout time.
taskQueue & timerQueue
In the conditional branch startTime > currentTime, the task is pushed to taskQueue and timerQueue, respectively. These two queues are actually the smallest heap structure we analyzed earlier. TaskQueue represents the currently scheduled task, while timerQueue represents the delayed taskQueue. During the task scheduling process, tasks in the timerQueue are continuously transferred to the taskQueue, which will be discussed later.
The specific process of scheduling
We can see that when a task is inserted into the schedule queue, if it is not scheduled at that time, the requestHostCallback method is called to start scheduling and a flushwork is passed in as the input function.
requestHostCallback = function(callback) {
// The incoming callback is cached
scheduledHostCallback = callback;
// Whether it is in the message loop
if(! isMessageLoopRunning) { isMessageLoopRunning =true;
port.postMessage(null); }};Copy the code
RHC only caches the callback (flushwork) function. And sent an empty message. So the focus is on what this port is. This is where React emulates the requestIdleCallback.
MessageChannel simulates rIC to achieve cyclic scheduling
For those unfamiliar with MessageChannel, check it out. Let’s take a look at how Scheduler is used.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
Copy the code
As you can see, when a message is generated using port.postMessage, the function that actually processes the message is performWorkUntilDeadline.
let isMessageLoopRunning = false;
let scheduledHostCallback = null;
const performWorkUntilDeadline = () = > {
// scheduledHostCallback is assigned by scheduledHostCallback
if(scheduledHostCallback ! = =null) {
const currentTime = getCurrentTime();
// [Q]: End time = Current time + yieldInterval
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
// Are there any remaining tasks? ScheduledHostCallback may be flushwork
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if(! hasMoreWork) {// No more tasks stop the loop and clear the scheduledHostCallback reference
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there are still tasks, continue to send messages. It's like a recursive operation
port.postMessage(null); }}catch (error) {
// If a task goes wrong. Skip the next task and throw an error
port.postMessage(null);
throwerror; }}else {
// Reset the loop state
isMessageLoopRunning = false;
}
// [Q]: I don't know what this is
needsPaint = false;
};
Copy the code
As usual, there are a few things that need to be analyzed carefully.
yieldInterval
From the name and usage, I think it should stand for the execution time of the task.
// The default is 5
let yieldInterval = 5;
forceFrameRate = function (fps) {
/ /??? Look down on me 144Hz
if (fps < 0 || fps > 125) {
console['error'] ('forceFrameRate takes a positive int between 0 and 125, ' +
'forcing framerates higher than 125 fps is not unsupported',);return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
yieldInterval = 5; }};Copy the code
ForceFrameRate is an EXTERNAL API interface used to dynamically configure the execution period of scheduled tasks.
deadline & needsPaint
let deadline = 0;
let maxYieldInterval = 300;
let needsPaint = false;
if( enableIsInputPending && navigator ! = =undefined&& navigator.scheduling ! = =undefined&& navigator.scheduling.isInputPending ! = =undefined
) {
const scheduling = navigator.scheduling;
shouldYieldToHost = function () {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
// There is no time. You may want to cede control to the main thread so that the browser can perform high-priority tasks, primarily drawing and user input
// Therefore, if there is drawing or user input, it should be abandoned and put back true
// If neither exists, it is possible to reduce production while maintaining responsiveness
// But there are draw status updates or other mainline tasks that are not 'requestPaint' initiated (such as network events)
// At some point control must be ceded
if (needsPaint || scheduling.isInputPending()) {
// Pending drawing or user input
return true;
}
// There is no drawing or input to be processed. But control also needs to be released when maximum production intervals are reached
return currentTime >= maxYieldInterval;
} else {
return false; }}; requestPaint =function () {
needsPaint = true;
};
} else {
shouldYieldToHost = function () {
return getCurrentTime() >= deadline;
};
requestPaint = function () {}; }Copy the code
The first thing to make clear is that shouldYieldToHost and requestPaint are interface functions that Scheduler provides. Specific use will be analyzed in place later.
Deadline is used to check if the schedule timed out in shouldYieldToHost. By default, compare currentTime and Deadline. However, in an environment that supports navigator.scheduling, React will have more considerations, that is, browser drawing and user input should be limited response, otherwise the scheduling time can be appropriately extended.
Here is a summary of the scheduling start process, so that the brain is not confused.
requestHostCallback
Prepare tasks to be performedscheduledHostCallback
requestHostCallback
Example Start the task scheduling cycleMessageChannel
Receive the message and invokeperformWorkUntilDeadline
Perform a taskperformWorkUntilDeadline
Is calculated firstdeadline
. And then execute the mission- After a task is executed, the return value is used to determine whether there is a next task. If so, recursive execution is achieved through a message loop
performWorkUntilDeadline
. Otherwise, the message loop ends
The logic of task scheduling cycle execution was only analyzed. The specific task is to flushWork, the reference function of scheduledHostCallback.
Task execution
function flushWork(hasTimeRemaining, initialTime) {
if (enableProfiling) {
markSchedulerUnsuspended(initialTime);
}
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) {
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if(currentTask ! = =null) {
const currentTime = getCurrentTime();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throwerror; }}else {
// No catch in prod codepath.
// The official comment says that the build environment does not catch errors thrown by workLoop
returnworkLoop(hasTimeRemaining, initialTime); }}finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
if (enableProfiling) {
constcurrentTime = getCurrentTime(); markSchedulerSuspended(currentTime); }}}Copy the code
FlushWork’s work is simple. It just resets some flags and returns the result of the workLoop execution. So the focus must be on this function.
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// [Q]: What is this?
advanceTimers(currentTime);
// Take out the top task. The highest priority task
currentTask = peek(taskQueue);
while( currentTask ! = =null &&
// debug is used! (enableSchedulerDebugging && isSchedulerPaused) ) {if (
// If the task has not expired and the deadline of the current scheduling period has arrived, the task will be postponed to the next scheduling period. shouldYieldToHost
currentTask.expirationTime > currentTime &&
// These two are analyzed previously; HasTimeRemaining is always true, so what's the point??(! hasTimeRemaining || shouldYieldToHost()) ) {break;
}
const callback = currentTask.callback;
if(callback ! = =null) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
// Calculate whether the current task has timed out
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
/ / [Q] : implement the callback, such as performConcurrentWorkOnRoot render process analysis to the front
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// continuationCallback replaces the callback of the current task
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
// continuationCallback is not valid and is ejected from the task queue
// To prevent the task from being picked up elsewhere, judge
if(currentTask === peek(taskQueue)) { pop(taskQueue); }}// em.... Is it
advanceTimers(currentTime);
} else {
// The task was cancelled, and the task was ejected
// review the case of cancelCallback called "ensureRootIsScheduled"
pop(taskQueue);
}
// Take the task from the top again
// Note: If continuationCallback is valid, there is no pop current task. This time, it's still the current task
currentTask = peek(taskQueue);
}
// performWorkUntilDeadline = hasMoreWork
if(currentTask ! = =null) {
return true;
} else {
// [Q] : Checks whether the task in the delay queue is expired
let firstTimer = peek(timerQueue);
if(firstTimer ! = =null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false; }}Copy the code
The general process notes are detailed. As usual, analyze a few problems with the notes.
advanceTimers
function advanceTimers(currentTime) {
// Iterate over tasks in timerQueue; Move timed tasks to the taskQueue
let timer = peek(timerQueue);
while(timer ! = =null) {
if (timer.callback === null) {
// The task was cancelled
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// Timeout task transfer
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer, currentTime);
timer.isQueued = true; }}else {
// Continue to hang undated
return; } timer = peek(timerQueue); }}Copy the code
The wookLoop function entry calls advanceTimers for the first time to reorganize tasks and refresh the task queue. After that, each call in the while takes a certain amount of time to execute, so the task queue needs to be refreshed again after execution.
continuationCallback
First, continuationCallback is determined by callback. The return value of callback may be a function that indicates that the current task should be reprocessed. I’ll leave you with a question that we’ll explore further when we look at the actual implementation of callback
requestHostTimeout & handleTimeout
At the end of the wookLoop, currentTask === NULL checks whether the task in the delay queue has expired.
requestHostTimeout = function (callback, ms) {
taskTimeoutID = setTimeout(() = > {
callback(getCurrentTime());
}, ms);
};
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
// Rearrange the task queue
advanceTimers(currentTime);
// isHostCallbackScheduled is true. It means there's a new mission coming in
if(! isHostCallbackScheduled) {Execute if advanceTimers above comb out expired delayed tasks to the task queue
if(peek(taskQueue) ! = =null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
// Otherwise call the method recursively
const firstTimer = peek(timerQueue);
if(firstTimer ! = =null) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); }}}}Copy the code
As you can see, this is actually after the task in the task queue has completed. Recursively queries whether there are expired tasks in the delay queue. If there are expired tasks, they are transferred to the task queue and executed.
At this point, the complete process of Scheduler from task listing, to circular scheduling, to task execution has been analyzed. Make a simple process summary:
unstable_scheduleCallback
Creates a task and pushes it to the delay queue if the task is delayedtimerQueue
Otherwise, the task is pushed to the queuetaskQueue
- Is called if the task created is a deferred task
requestHostTimeout
Methods usingsetTimeout
toRecursively detects whether a task is expired. Otherwise, the task is scheduledrequestHostCallback
requestHostCallback
throughMessageChannel
theport2
Send a message toport1
, the specific processing function isperformWorkUntilDeadline
performWorkUntilDeadline
The deadline of this dispatch will be calculated and used at the same timeMessage loopTo perform tasks recursively- Task specific handling by
wookLoop
The execution. It removes tasks from the task queuetaskQueue
The top of the heap is removed and executed in turn. Is called if the task queue is emptyrequestHostTimeout
Enable recursive detection.
Reconciler
After analyzing the Scheduler’s logic, the Reconciler’s logic is then analyzed. Much of our well-worn logic for Diff updates takes place in the Reconciler phase, where a lot of component updating is calculated and optimized.
The scheduling process of Scheduler is analyzed above. And specific implement the callback is performConcurrentWorkOnRoot in the Scheduler. Let’s take a look
// The entry function called by the Scheduler
function performConcurrentWorkOnRoot(root, didTimeout) {
/ / reset
currentEventTime = NoWork;
if (didTimeout) {
// The task timed out
const currentTime = requestCurrentTimeForUpdate();
// Mark the expiration time as current to process expired work synchronously in a single batch.
markRootExpiredAtTime(root, currentTime);
// Schedule a synchronization task
ensureRootIsScheduled(root);
return null;
}
// Get the next expiration (update) time. This will be judged as necessary to perform this rendering
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
if(expirationTime ! == NoWork) {const originalCallbackNode = root.callbackNode;
// TODO:Refresh passive Hooks
flushPassiveEffects();
// If the root or expiration time has changed, the existing stack is discarded and a new stack is prepared. Otherwise, we'll pick up where we left off.
if( root ! == workInProgressRoot || expirationTime ! == renderExpirationTime ) {// [Q]: reset data;
// Set renderExpirationTime to expirationTime
// Copy root.current to workInProgress, etc
prepareFreshStack(root, expirationTime);
startWorkOnPendingInteractions(root, expirationTime);
}
if(workInProgress ! = =null) {
/ / to omit...
do {
try {
workLoopConcurrent();
break;
} catch(thrownValue) { handleError(root, thrownValue); }}while (true);
/ / to omit...
}
if(workInProgress ! = =null) {
// There is still work to be done. Exit without submission.
stopInterruptedWorkLoopTimer();
} else {
stopFinishedWorkLoopTimer();
const finishedWork: Fiber = ((root.finishedWork =
root.current.alternate): any);
root.finishedExpirationTime = expirationTime;
/ / commit; Start the Renderer processfinishConcurrentRender( root, finishedWork, workInProgressRootExitStatus, expirationTime, ); }}return null;
}
Copy the code
The system checks whether the task times out. If the task times out, the system synchronizes the task to prevent task interruption. If there is no timeout, do some initialization in prepareFreshStack first. The workLoopConcurrent loop is then entered.
prepareFreshStack
// The expiration time of this render
let renderExpirationTime: ExpirationTime = NoWork;
function prepareFreshStack(root, expirationTime) {
/ / to omit...
if(workInProgress ! = =null) {
// workInProgress not empty indicates that there was an interrupted task before. To give up
let interruptedWork = workInProgress.return;
while(interruptedWork ! = =null) {
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
// Copy wIP from current; And reset effectList
workInProgress = createWorkInProgress(root.current, null);
// Set renderExpirationTime to the next expiration time
renderExpirationTime = expirationTime;
/ / to omit...
}
Copy the code
If the current WIP is not empty, it indicates that the last interrupted task was performed. Cancel the interrupted task by backtracking all the way up to the root node. Then get the expiration date of the next task from FiberRoot at the same time and assign renderExpirationTime as the expiration date of this render.
workLoopConcurrent
The code for workLoopConcurrent was posted at the beginning of this article, so take a look again
function workLoopConcurrent() {
while(workInProgress ! = =null && !shouldYield()) {
// workInProgress is FiberRoot Fiber
// The last return value (subfiber) is then used as an input parameterworkInProgress = performUnitOfWork(workInProgress); }}Copy the code
The work of workLoopConcurrent is mainly to compare current and workInProgress Fiber trees. Put an effectTag on the changed Fiber in the WIP. DOM nodes are also updated/created from the bottom up to form an off-screen DOM tree, which is processed by the Renderer.
Recursion based on loop
Post a truncated version of the code flow before familiarizing yourself with it. Here is not according to the routine card, first according to personal understanding to make a summary. This with a general idea of the structure may be better to understand the subsequent source code.
function performUnitOfWork(unitOfWork: Fiber) :Fiber | null {
// Old Fiber for comparison
const current = unitOfWork.alternate;
/ / to omit...
// [Q]: Process the current Fiber node and return the next child node Fiber
let next = beginWork(current, unitOfWork, renderExpirationTime);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// There are no child nodes
if (next === null) {
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
return next;
}
// Try to complete the current Fiber, then move to the next level. If there are no more siblings, return parent fiber.
function completeUnitOfWork(unitOfWork: Fiber) :Fiber | null {
workInProgress = unitOfWork;
do {
// Old Fiber for comparison
const current = workInProgress.alternate;
const returnFiber = workInProgress.return;
// Check if the work completed or if something threw.
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
// [Q]: Create/update the node instance corresponding to the current Fiber
let next = completeWork(current, workInProgress, renderExpirationTime);
stopWorkTimer(workInProgress);
resetChildExpirationTime(workInProgress);
if(next ! = =null) {
// New child nodes are generated
return next;
}
// [Q]: A list of effectLists is created
// Start with...
} else {
// An exception was thrown. The decision to catch or throw an exception depends on whether it is boundary
/ / to omit...
}
const siblingFiber = workInProgress.sibling;
// Whether sibling nodes exist
if(siblingFiber ! = =null) {
return siblingFiber;
}
workInProgress = returnFiber;
} while(workInProgress ! = =null);
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
return null;
}
Copy the code
Begin the beginWork node operation and create a child node. The child node is returned as Next or if there is a next. After returning to workLoopConcurrent, workLoopConcurrent determines whether it is expired or not, and if it is not, it calls the method again.
If next does not exist, it means that the current node has traversed down to the bottom of the child node, meaning that the branch of the subtree has been traversed and is ready to do its job. Perform completeUnitOfWork in the following steps
completeUnitOfWork
First callcompleteWork
Create/update the currentFiber
Corresponding node instances (such as native DOM nodes)instance
, while the child has been updatedFiber
Is inserted into theinstance
Form an off-screen render tree.- There are currently
Fiber
The node iseffectTag
Appends it toeffectList
In the - Find if there are
sibling
Sibling node, if yes, return the sibling node, because this node may also have child nodes, need to passbeginWork
Perform operations. - If there are no sibling nodes. All the way up and back until
root
Node or found on a nodesibling
Sibling nodes. - If the
root
, so is its returnnull
, which means the whole tree traversal is done, okcommit
. If a sibling is encountered in the middle, it is the same as the first3
step
The text may not be very clear, but look directly at an example:
The execution sequence is:
The text “hello” node does not execute beginWork/completeWork because React handles Fiber with a single text child node
1. App beginWork
2. div Fiber beginWork
3. span Fiber beginWork
4. span Fiber completeWork
5. div Fiber completeWork
6. p Fiber beginWork
7. p Fiber completeWork
8. App Fiber completeWork
Copy the code
beginWork
BeginWork has analyzed the logic corresponding to the mount stage in the previous analysis of setState. So I’ll just analyze the update logic here. Let’s take a look at the general work of beginWork.
/ * * *@param {*} Current Old Fiber *@param {*} WorkInProgress new Fiber *@param {*} RenderExpirationTime Expiration time *@returns Sub-component Fiber */
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) :Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
// Try to reuse the current node
if(current ! = =null) {
/ / to omit...
/ / reuse current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
workInProgress.expirationTime = NoWork;
// If you cannot reuse it, update or mount it
switch (workInProgress.tag) {
/ / to omit...
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
/ / to omit...}}Copy the code
We continue with the updateClassComponent we analyzed earlier to analyze the update flow.
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps,
renderExpirationTime: ExpirationTime,
) {
// Process the context logic ahead of time. Omit...
// An instance of the component
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
// mount. wip.effectTag = Placement
/ / to omit...
} else {
// update. wip.effectTag = Update | Snapshot
/ / before invoking render the life cycle of getDerivedStateFromProps | UNSAFE_componentWillReceiveProps (maybe two)
// Then call shouldComponentUpdate to see if an update is needed
// Finally update props and state
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
}
// execute render to create a sub-fiber.
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
return nextUnitOfWork;
}
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderExpirationTime: ExpirationTime,
) {
// The reference should be updated, even if shouldComponentUpdate returns false
markRef(current, workInProgress);
constdidCaptureError = (workInProgress.effectTag & DidCapture) ! == NoEffect;// Reuse current without updating and without sending errors
if(! shouldUpdate && ! didCaptureError) {if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
/ / reuse current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
const instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner.current = workInProgress;
let nextChildren = instance.render();
// PerformedWork is provided for React DevTools to read
workInProgress.effectTag |= PerformedWork;
if(current ! = =null && didCaptureError) {
// Error.
/ / to omit...
} else {
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
}
workInProgress.memoizedState = instance.state;
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
return workInProgress.child;
}
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
if (current === null) {
// Mount components
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// Update componentsworkInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderExpirationTime, ); }}Copy the code
Update workinprogress. child workinprogress. child workinprogress. child workinprogress. child workinprogress. child
In fact, mountChildFibers and reconcileChildFibers both point to the same function for reconcileChildFibers. The difference is the second parameter currentFirstChild. If null, a new Fiber object is created, otherwise reuse and update props. For example, the reconcileSingleElement is used to deal with situations where there is only a single node.
completeWork
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) :Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
/ / to omit...
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// Whether the DOM node corresponding to the fiber node exists
// update
if(current ! = =null&& workInProgress.stateNode ! =null) {
// Calculate the new update Ue for the WIP
// updateQueue is an array of odd-indexed prop keys and even-indexed prop values
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// mount
if(! newProps) {return null;
}
const currentHostContext = getHostContext();
// Is it server rendering
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
/ / to omit...
} else {
// Generate the real DOM
let instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// Insert the descendant DOM node into the newly generated DOM node and form an off-screen DOM tree from bottom to top
appendAllChildren(instance, workInProgress, false.false);
workInProgress.stateNode = instance;
// processing props similar to updateHostComponent
if( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); }}if(workInProgress.ref ! = =null) { markRef(workInProgress); }}return null;
}
/ / to omit...}}Copy the code
First, as with beginWork, check whether current === NULL is mount or update.
Update, mainly do the following things, specific source code diffProperties:
- Calculate the new
STYLE prop
- Calculate the new
DANGEROUSLY_SET_INNER_HTML prop
- Calculate the new
CHILDREN prop
Each time a new prop is computed, its propKey is stored in the array updatePayload paired with nextProp. Finally, assign updatePayload to wip.updatequeue.
When mounting, there are many things to deal with, which are roughly as follows:
createInstance
: in order toFiber
The node generates the corresponding truthDOM
nodeappendAllChildren
: the childrenDOM
Insert the newly generated nodeDOM
Nodes. To form the whole from the bottom upDOM
The treefinalizeInitialChildren
In:setInitialProperties
Handles event registration in. insetInitialDOMProperties
According to theprops
Initialize theDOM
attribute
Value of note is the appendAllChildren method. Because completeWork is an upward traceback process, each appendAllChildren call inserts the generated descendant DOM node under the currently generated DOM node. When you go back to the root node, the entire DOM tree is updated.
effectList
After each completeWork, a node is processed. As mentioned earlier, the Reconciler attaches effecttags to the changed nodes, which are used to update the Reconciler in the Renderer based on the execution of the node’s EffectTags.
So in the upper function of completeWork, completeUnitOfWork (the code omitted earlier), the completeWork maintains a one-way linked list of effectLists after each execution. Insert the linked list if the current Fiber has an effectTag.
If (returnFiber! == null && (returnFiber. EffectTag & Incomplete) === NoEffect) {// firstEffect is the head of the linked list if (returnFiber. FirstEffect === = null) { returnFiber.firstEffect = workInProgress.firstEffect; } / / lastEffect for linked list node if (workInProgress lastEffect! == null) { if (returnFiber.lastEffect ! == null) { returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; } returnFiber.lastEffect = workInProgress.lastEffect; } const effectTag = workInProgress.effectTag; // Skip NoWork and PerformedWork tags. The latter is provided to React Tools to read if (effectTag > PerformedWork) {if (returnFiber. LastEffect! == null) { returnFiber.lastEffect.nextEffect = workInProgress; } else { returnFiber.firstEffect = workInProgress; } returnFiber.lastEffect = workInProgress; }}Copy the code
This brings the Reconciler process to an end. Look back at the beginning of the summary, is it clear a few ~
Renderer(Commit)
The code in the Commit phase is relatively simple compared to the other two. Its entry in the previous analysis of task scheduling entrance end of performConcurrentWorkOnRoot finishConcurrentRender function. The final function called is commitRootImpl. Take a look at the code:
let nextEffect: Fiber | null = null;
function commitRootImpl(root, renderPriorityLevel) {
/ / to omit...
const finishedWork = root.finishedWork;
const expirationTime = root.finishedExpirationTime;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedExpirationTime = NoWork;
// commit Cannot be interrupted. Always done synchronously.
// Therefore, these can now be cleared to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
root.nextKnownPendingLevel = NoWork;
/ / to omit...
/ / get effectList
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if(finishedWork.lastEffect ! = =null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else{ firstEffect = finishedWork; }}else {
firstEffect = finishedWork.firstEffect;
}
if(firstEffect ! = =null) {
/ / to omit...
nextEffect = firstEffect;
do {
// [Q]: Execute snapshot = getSnapshotBeforeUpdate()
/ / the assignment for Fiber. StateNode. Instance. __reactInternalSnapshotBeforeUpdate = the snapshot
commitBeforeMutationEffects();
} while(nextEffect ! = =null);
/ / to omit...
nextEffect = firstEffect;
do {
// [Q]: Add, delete and modify DOM operations according to Fiber. EffectTag
// componentWillUnmount() is also called if the component is unmounted
commitMutationEffects(root, renderPriorityLevel);
} while(nextEffect ! = =null);
/ / to omit...
nextEffect = firstEffect;
do {
// [Q]: life cycle after render
// current === null ? componentDidMount : componentDidUpdate
commitLayoutEffects(root, expirationTime);
} while(nextEffect ! = =null);
stopCommitLifeCyclesTimer();
nextEffect = null;
// Tells the Scheduler to stop scheduling at the end of the frame so that the browser has a chance to draw.
requestPaint();
/ / to omit...
} else {
/ / to omit...
}
/ / to omit...
return null;
}
Copy the code
A lot of code is omitted, leaving the main content. The main logic is to take the Reconciler’s effectList and run it through it three times, doing the following:
- To obtain
Snapsshot
; Used forcomponentDidUpdate
The third parameter of - According to the
Fiber.effectTag
Perform specific operations on the component or DOM - Call the lifecycle functions of all components
commitBeforeMutationEffects
See commitBeforeMutationLifeCycles complete code, the components of tai for ClassComponent mainly logic is as follows:
const current = nextEffect.alternate;
finishedWork = nextEffect;
if (finishedWork.effectTag & Snapshot) {
if(current ! = =null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
constsnapshot = instance.getSnapshotBeforeUpdate( finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState, ); instance.__reactInternalSnapshotBeforeUpdate = snapshot; }}Copy the code
commitMutationEffects
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while(nextEffect ! = =null) {
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
// Set the literal content of the node to an empty string
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if(current ! = =null) {
// if ref is set to null, the ref will be set later, so the previous values on ref need to be cleared firstcommitDetachRef(current); }}let primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Remove Placement tags from effectTag
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
// componentWillUnmount
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
/ / to omit...} nextEffect = nextEffect.nextEffect; }}Copy the code
I don’t think there’s much to talk about. Note that commitDetachRef is called to clear the ref reference before starting. Different DOM operations are then performed for different effectTags.
- commitPlacement; A node is added. The calculation algorithm of node insertion position can be seen below.
commitWork
; According to theReconciler
indiffProperties
calculatedupdateQueue
arrayDOM
updatecommitDeletion
; This step calls each component under the subtree from the top downcomponentWillUnmount
function
commitLayoutEffects
function commitLayoutEffects(root: FiberRoot, committedExpirationTime: ExpirationTime,) {
while(nextEffect ! = =null) {
setCurrentDebugFiberInDEV(nextEffect);
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
recordEffect();
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(
root,
current,
nextEffect,
committedExpirationTime,
);
}
if(effectTag & Ref) { recordEffect(); commitAttachRef(nextEffect); } resetCurrentDebugFiberInDEV(); nextEffect = nextEffect.nextEffect; }}function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedExpirationTime: ExpirationTime,
) :void {
switch (finishedWork.tag) {
// ...
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
constprevState = current.memoizedState; instance.componentDidUpdate( prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); }}const updateQueue = finishedWork.updateQueue;
if(updateQueue ! = =null) {
// Call the setState registered callback function
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
// ...}}Copy the code
Again, traverse each Fiber node. If it’s ClassComponent, call the lifecycle method. For the updated ClassComponent, we need to determine if the setState called has a callback, and if so, we need to call it here as well. Finally, commitAttachRef is called to update the REF reference.
This is the end of the Commit phase.
To be honest, the React source code is really a lot. It takes a lot of time and effort to analyze each point in full detail. This paper only analyzes a general process, many details are not analyzed in place. I’ll spend some time exploring some of the details later. In the final analysis, it is only from surface to surface, not from surface to point analysis. Many views are personal understanding, write out for learning exchange, there is something wrong, please also put forward suggestions.