The purpose of learning source code
A lot of times we will always struggle to learn the source code, will be volume and so on, but I see, learn not to learn or depend on yourself, no matter what kind of purpose you are to learn, in short, learned is your own. From the utilitarian point of view, learning source code is to interview, in order to get a better salary. But as a tech person, do you have the desire to delve deeper, always want to know how the magic is implemented, so let’s learn with some “purpose”.
Some of the problems
First from the simplest life cycle problems to learn source code, before learning, with some problems to see, help to solve some of the usual may not be able to think of the problem, or even interview questions encountered in the interview, the following are some questions before they began to learn:
- How does the React mount process work? What’s the difference between a class component and a Function component?
- React’s lifecycle method execution sequence.
- Why take will life cycle are not safe, such as componentWillMount componentWillUpdate componentWillReceiveProps will add the prefix UNSAFE.
- UseEffect and useLayout.
The React of architecture
React’s initial rendering to the page can be divided into two stages:
- Render/reconcile phase
- The commit phase
In the Render phase, Fiber tree is formed, and in the COMMIT phase, the effectList of the Fiber tree is traversed to establish corresponding DOM nodes and render them on the page. This is the first mounting process of React.
This paper also focuses on two processes to try to solve the problems raised at the beginning. The source version is 17.0.2.
Let’s start with an official life cycle chart.
The React lifecycle is divided into three phases: mount phase, update phase, and uninstall phase.
Let’s break it down.
Mount the stage
As you can see from the figure above, React mainly includes four life cycles in the mount phase, which are listed in the following order:
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
React (vite) {React (vite) {React (vite);
class App extends React.Component {
constructor(props) {
super(props);
debugger;
console.log('father constructor'.this);
this.state = { count: 0 };
}
setCount = () = > {
this.setState({ count: this.state.count + 1 });
};
render() {
console.log('father render');
return (
<div className="App">
<img src={logo} className="App-logo" alt="logo" />
<div>
<button onClick={this.setCount}>
count is: {this.state.count}
</button>
</div>
</div>)}}Copy the code
There are so many call stacks going through from the entry Render function to the constructor App. Instead of worrying about what happens in the middle, let’s focus on the constructor.
The React documentation states that constructor is generally used to initialize state or do some function binding work, and setState is not recommended for use in constructor. We will setState in constructor instead. Modify the above constructor.
constructor(props) {
super(props);
console.log('father constructor'.this);
this.state = { count: 0 };
this.setState({
count: 0
});
}
Copy the code
Then you notice that a warning is added to the console
Warning: setState was executed
Component.prototype.setState = function (partialState, callback) {
if(! (typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {{throw Error( "setState(...) : takes an object of state variables to update or a function which returns an object of state variables."); }}this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
var ReactNoopUpdateQueue = {
enqueueSetState: function (publicInstance, partialState, callback, callerName) {
warnNoop(publicInstance, 'setState'); }};Copy the code
Why does setState just print the warning, which is not what we expected, so we put the breakpoint in the click event again, and then we find another setState
// The setState prototype method is exactly the same
var classComponentUpdater = {
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
update.payload = payload;
if(callback ! = =undefined&& callback ! = =null) {
{
warnOnInvalidCallback(callback, 'setState'); } update.callback = callback; } enqueueUpdate(fiber, update); scheduleUpdateOnFiber(fiber, lane, eventTime); }};Copy the code
This conforms to our expectations for setState, and if so, would explain why performing setState from constructor doesn’t work.
But why do they do it differently? As you go along, the key is in the above constructClassInstance method, which executes something else besides the constructors
// There is some omitted code that is not needed for the moment
var isLegacyContextConsumer = false;
var unmaskedContext = emptyContextObject;
var context = emptyContextObject;
var contextType = ctor.contextType;
// If StrictMode is used, it will enter here
React instantiates react twice to help detect side effects
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
) {
disableLogs();
try {
new ctor(props, context);
} finally{ reenableLogs(); }}// where the constructor is executed
var instance = new ctor(props, context);
// Get the state of the fiber tree
varstate = workInProgress.memoizedState = instance.state ! = =null&& instance.state ! = =undefined ? instance.state : null;
// This is one of the ways to distinguish the two forms of setState
adoptClassInstance(workInProgress, instance);
return instance;
Copy the code
function adoptClassInstance(workInProgress: Fiber, instance: any) :void {
// The updater assigns the setState method to the instance
instance.updater = classComponentUpdater;
// Fiber tree association instance
workInProgress.stateNode = instance;
// The instance needs to access the fiber to schedule updates
// Instance. _reactInternals = workInProgress
setInstance(instance, workInProgress);
}
Copy the code
It should be clear at this point that only after the constructor method has been executed can the actual setState method be placed on the instance.
Okay, with the constructor out of the way, move on to the next method, getDerivedStateFromProps. So where is this method called? Go back to the constructClassInstance method above, the updateClassComponent method, and see what’s going on here
function updateClassComponent(
current, // Current is null when the fiber tree is first mounted
workInProgress, // Fiber tree built in memory
Component, // This is our root App
nextProps, // The props property, the root application is not set to props, is empty
renderLanes, // Call priority is dependent
) {
// There is some logic about the context removed
// This instance is null
// Only after the constructor is executed will the instance be assigned to the stateNode
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
// This entry is not found
if(current ! = =null) {
current.alternate = null;
workInProgress.alternate = null;
workInProgress.flags |= Placement;
}
// In the initial process, the instance needs to be constructed
constructClassInstance(workInProgress, Component, nextProps);
// This is the entry method for getDerivedStateFromProps
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes,
);
} else {
// setState update goes here
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
// Complete the current fiber node and return to the next unit of work
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
return nextUnitOfWork;
}
Copy the code
When you get to this point, you need to connect the methods in sequence. Otherwise you might not understand why React does this. Here’s a simple flow chart:
Let’s try to describe the work done at this stage in a simple and understandable sentence:
First, React uses a dual-cache mechanism. The node pointer displayed on the page is current, and the node built in memory is workingInProgress. The current pointer is null when first mounted, so React does a depth-first traversal of the node tree created by JSX, creating a Fiber tree. Compare the old Fiber tree created by JSX, but creating current for the first time is empty. So just add the properties to the new Fiber tree. After the Fiber tree is completed, the Render phase ends and the commit phase begins.
Here is just a brief description of the render phase of the implementation of the flow, there are many details, but not today’s focus, interested can go to see this article.
The React legacy mode is used as a process, and the frequently mentioned interruptible updates are actually released concurrently by React. The React mode is also described in the documentation.
- Legacy mode: reactdom.render (
, rootNode). This is the way the React app is currently used. There are currently no plans to remove this schema, but it may not support these new features. - Blocking mode: ReactDOM createBlockingRoot (rootNode.) render (< App / >). It is currently being tested. As the first step in the migration to Concurrent mode.
- Concurrent mode: reactdom.createroot (rootNode).render(
). React is currently being tested as the default development mode when it stabilizes. This mode opens up all the new features.
Okay, back to the life cycle. Now that we’ve found the portal function for getDerivedStateFromProps, let’s see what this portal function does:
function mountClassInstance(workInProgress, ctor, newProps, renderLanes,) :void {
// Check if the class class defines the Render method, getInitialState, getDefaultProps, propTypes, contextType format, and some lifecycle method checks
checkClassInstance(workInProgress, ctor, newProps);
const instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState;
instance.refs = emptyRefsObject;
// Initialize updateQueue, which is a linked list where all updates are placed
initializeUpdateQueue(workInProgress);
// Don't assign props to state directly
if (instance.state === newProps) {
const componentName = getComponentName(ctor) || 'Component';
if(! didWarnAboutDirectlyAssigningPropsToState.has(componentName)) { didWarnAboutDirectlyAssigningPropsToState.add(componentName);console.error(
'%s: It is not recommended to assign props directly to state ' +
"because updates to props won't be reflected in state. " +
'In most cases, it is better to use props directly.', componentName, ); }}// There are some warnings about unsafe life cycles and context
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
// The getDerivedStateFromProps lifecycle function is executed here
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
instance.state = workInProgress.memoizedState;
}
// If there are new lifecycle methods, getDerivedStateFromProps and getSnapshotBeforeUpdate,
// Then componentWillMount will not be called, otherwise it will be called
if (
typeofctor.getDerivedStateFromProps ! = ='function' &&
typeofinstance.getSnapshotBeforeUpdate ! = ='function' &&
(typeof instance.UNSAFE_componentWillMount === 'function' ||
typeof instance.componentWillMount === 'function')
) {
callComponentWillMount(workInProgress, instance);
// If we had additional state updates during this life-cycle, let's
// process them now.processUpdateQueue(workInProgress, newProps, instance, renderLanes); instance.state = workInProgress.memoizedState; }}Copy the code
Finally, we can see that when there is a new lifecycle API, none of the lifecycles with Will will be called. We’ll look at the Will lifecycle here later, but let’s look at where getDerivedStateFromProps is called:
function applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, nextProps) {
var prevState = workInProgress.memoizedState;
var partialState = getDerivedStateFromProps(nextProps, prevState);
{
// A warning is generated if getDerivedStateFromProps does not return a value
warnOnUndefinedDerivedState(ctor, partialState);
}
// This merges the object returned by getDerivedStateFromProps with the previous state
var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;
}
Copy the code
The logic is straightforward: after executing the life cycle method getDerivedStateFromProps, merge its return value with the previous state. Note the applyDerivedStateFromProps method will be invoked again in the update phase. The only function of getDerivedStateFromProps is to update state when the props changes. This will be analyzed again in the update phase.
The getDerivedStateFromProps and componentWillMount methods are not called at the same time. Comment out the getDerivedStateFromProps method and add componentWillMount to see how it executes:
function callComponentWillMount(workInProgress, instance) {
var oldState = instance.state;
// Call the willMount lifecycle method
if (typeof instance.componentWillMount === 'function') {
instance.componentWillMount();
}
if (typeof instance.UNSAFE_componentWillMount === 'function') {
instance.UNSAFE_componentWillMount();
}
// If state is not equal before and after willMount, it indicates that state is reassigned in willMount, causing the state reference to change
if(oldState ! == instance.state) { { error('%s.componentWillMount(): Assigning directly to this.state is ' + "deprecated (except inside a component's " + 'constructor). Use setState instead.', getComponentName(workInProgress.type) || 'Component');
}
// React will change the assignment statement to a setState call
classComponentUpdater.enqueueReplaceState(instance, instance.state, null); }}Copy the code
There’s a sentence in the official document
ComponentWillMount is called before Render (), so calling setState() synchronically in this method does not trigger additional rendering.
The setState update method does not trigger the instance’s render, since the current tree and the in-memory tree are a reference, indicating that the current stage is the render stage and therefore the render method will not be executed.
After executing getDerivedStateFromProps or componentWillMount, it’s time to call the Render method. Back up there is a finishClassComponent method in the updateClassComponent method, which is where the Render method is called. Take a look at this method:
function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
/ / update the ref
markRef(current, workInProgress);
vardidCaptureError = (workInProgress.flags & DidCapture) ! == NoFlags;// shouldComponentUpdate should render fiber again if it returns false
if(! shouldUpdate && ! didCaptureError) {// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
var instance = workInProgress.stateNode; // Rerender
ReactCurrentOwner$1.current = workInProgress;
var nextChildren;
if (didCaptureError && typeofComponent.getDerivedStateFromError ! = ='function') {
// If an error is caught but getDerivedStateFromError is not defined, unload all child elements.
// componentDidCatch will schedule updates to rerender fallback. This is temporary until we will migrate to the new API.
// TODO: Warn in a future release.
nextChildren = null; { stopProfilerTimerIfRunning(); }}else {
{
setIsRendering(true);
nextChildren = instance.render();
// Render is called twice, react is interpreted to detect side effects, and the render log will not be printed
if ( workInProgress.mode & StrictMode) {
disableLogs();
try {
instance.render();
} finally {
reenableLogs();
}
}
setIsRendering(false); }}if(current ! = =null && didCaptureError) {
// If you are recovering from an error, you can coordinate without reusing any existing child elements.
// Conceptually, ordinary child elements and child elements shown in errors are two different sets,
// Therefore, ordinary child elements should not be reused even if their identities match.
forceUnmountCurrentAndReconcile(current, workInProgress, nextChildren, renderLanes);
} else {
// Coordinate the react element returned by render
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
// fiber remembers the value of state on the instance as a state cache
workInProgress.memoizedState = instance.state; // The context might have changed so we need to recalculate it.
// Return the child element of the current fiber
return workInProgress.child;
}
Copy the code
As you can see from the above flow chart, the React mount process is actually a top-down iterative process. In Legacy mode, this recursive update cannot be broken. Doing some computation in the Render method results in slow updates, especially when the browser freezes. Hence the need to keep the Render method pure and concise.
After executing the Render method, proceed to the commitRoot method and enter the COMMIT phase.
Summarize the commit phase is doing, the first is beforeMutation stage, this stage mainly did some variable assignment work, its internal is to call the commitBeforeMutationLifeCyles method, The life cycle of getSnapshotBeforeUpdate is executed, but note that this method is only called when the state or props changes, and is not called when the state is initialized for the first mount.
At the same time, try changing the root App component to a function component and adding a Child component
/ / the root component
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('qiugu');
const dom = useRef(null);
const clickHandler = () = > {
setCount(count+1);
setName(name= > name + count);
}
return (
<div className="App">
<img src={logo} className="App-logo" alt="logo" />
<Child name={name}/>
<div>
<button onClick={clickHandler} ref={dom}>
count is: {count}
</button>
</div>
</div>)}/ / child component
class Child extends React.Component<{name: string}, {}> {
constructor(props: {name: string}) {
super(props);
console.log('child constructor');
this.state = { age: 20 };
}
getSnapshotBeforeUpdate() {
console.log('child getSnapshotBeforeUpdate');
return {
name: 'qiugu2'}}componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevProps, prevState, snapshot, 'child componentDidUpdate');
console.log('child componentDidUpdate');
}
render() {
console.log('child render');
return <div>I am a child component name: {this.props. Name} Age: {this.state.age}</div>}}Copy the code
It turns out that the getSnapshotBeforeUpdate method is called the first time for the child component, but not the first time if the root component is a class component!
Then comes the mutation stage, which mainly deals with the side effect labels carried on fiber. This stage can be ignored for the time being. If you are interested, you can learn more about how to insert nodes. In the mutation phase, DOM elements will be inserted into the page, so DOM references such as ref can be accessed.
function commitMutationEffects(root, renderPriorityLevel) {
while(nextEffect ! = =null) {
setCurrentFiber(nextEffect);
var flags = nextEffect.flags;
if (flags & ContentReset) {
commitResetTextContent(nextEffect);
}
if (flags & Ref) {
var current = nextEffect.alternate;
if(current ! = =null) {
// Remove the ref referencecommitDetachRef(current); }}var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Placement:
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
break;
case PlacementAndUpdate:
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
var _current = nextEffect.alternate;
commitWork(_current, nextEffect);
break;
case Hydrating:
nextEffect.flags &= ~Hydrating;
break;
case HydratingAndUpdate:
nextEffect.flags &= ~Hydrating;
var _current2 = nextEffect.alternate;
commitWork(_current2, nextEffect);
break;
case Update:
var _current3 = nextEffect.alternate;
commitWork(_current3, nextEffect);
break;
case Deletion:
commitDeletion(root, nextEffect);
break; } resetCurrentFiber(); nextEffect = nextEffect.nextEffect; }}Copy the code
Finally, the layout stage, which is after the DOM element is mounted, the synchronous execution of methods such as componentDidUpdate and useLayoutEffect, and the dispatch of useEffect method.
function commitLayoutEffects(root, committedLanes) {
while(nextEffect ! = =null) {
setCurrentFiber(nextEffect);
var flags = nextEffect.flags;
if (flags & (Update | Callback)) {
var current = nextEffect.alternate;
// Implement lifecycle methods, including useEffect and useLayoutEffect for functional components
commitLifeCycles(root, current, nextEffect);
}
if (flags & Ref) {
// Update references to DOM elements and refcommitAttachRef(nextEffect); } resetCurrentFiber(); nextEffect = nextEffect.nextEffect; }}Copy the code
CommitLifeCycles: useEffect ()
function commitLifeCycles(finishedRoot, current, finishedWork, committedLanes) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block:
{
// Function components enter here
// This is the entry to the useLayoutEffect callback function
commitHookEffectListMount(Layout | HasEffect, finishedWork);
// This is the entry to the useEffect callback
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent:
{
var instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
// The current is mounted or updated based on whether current exists
if (current === null) {
// Executes the componentDidMount lifecycle method
instance.componentDidMount();
} else {
// Executes the componentDidUpdate method
var prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps);
varprevState = current.memoizedState; instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate); }}var updateQueue = finishedWork.updateQueue;
if(updateQueue ! = =null) {
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
// Other fiber types are not considered for the time being
case HostRoot:
return;
case HostComponent:
return;
case HostText:
return;
case HostPortal:
return;
case Profiler:
return;
case SuspenseComponent:
return;
case SuspenseListComponent:
case IncompleteClassComponent:
case FundamentalComponent:
case ScopeComponent:
case OffscreenComponent:
case LegacyHiddenComponent:
return; }}Copy the code
function schedulePassiveEffects(finishedWork) {
var updateQueue = finishedWork.updateQueue;
varlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
if(lastEffect ! = =null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
var _effect = effect,
next = _effect.next,
tag = _effect.tag;
if ((tag & Passive$1) !== NoFlags$1&& (tag & HasEffect) ! == NoFlags$1) {
// The useEffect callback and offload methods are queued and not executed directly
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
Copy the code
After layout, execute the useLayoutEffect or componentDidMount method, depending on whether the component type is a function or a class component. If the useEffect callback exists in the function component, it will be queued and React will execute it when appropriate. Therefore, it is clear that for useLayoutEffect and useEffect, one is synchronous and the other is asynchronous, so if you perform a time-consuming operation in a synchronous method, it will affect the rendering speed of the page, and this is not recommended. On the other hand, the useEffect method has this problem.
This is the end of the commit phase. Some of the details of how to create Fiber and how to depth-first traverse the entire tree were omitted, but in general, we hope readers will have a new understanding of the React lifecycle and answer some of the questions raised above.
To be continued
Unconsciously, there is a bit too much to write, and the update and uninstall phases are not mentioned. This is the use of every day off work time constantly debugging source, view information point by point accumulation, whether it is their own or readers, need time to digest these content, so the update and uninstall into the next chapter to continue to analyze.