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.