Share your recent experience of learning react source code

Posted by ninevolt1 on Thu, 17 Feb 2022 21:21:42 +0100

Share your recent experience of learning react source code

Write in front: I have been learning the source code of React recently. The purpose of writing this article is two:

  1. Share your learning experience and hope that students with relevant learning needs will have an address to find and avoid detours.
  2. Convert the learned content into text output for later review (so most of the text in the text may have the problem of mechanically copying, and only a few are their own understanding)

I will divide this period into the following stages:

  1. According to the tutorial, write a simple React
  2. Compare the React call stack to understand the general workflow of React
  3. Download the React source code and debug while learning according to the React source code analysis tutorial
  4. Review the tutorial interpretation again, and write detailed notes in the React source code in combination with other materials (including but not limited to the official website, other interpretation materials and videos)
  5. Write a note about the source code of React (what you are doing)

Once again, this article is mainly to share the learning manager. Students who want to systematically learn the source code of React can refer to the excellent articles at the end of the article~

How to write a simple react framework

Just before learning the source code of react, I suggest you manually implement a simple react. It is recommended to follow the following video tutorial.

react17 source code training camp

Follow the source code of the video tutorial

How to debug the source code of React

  1. First, clone the react source code locally from the react official. I use V17.0.2 Version of
# Pull code
1. git clone https://github.com/facebook/react.git
# You can also use cnpm mirroring
2. git clone https://github.com.cnpmjs.org/facebook/react 
# Or use the image of the code cloud
3. git clone https://gitee.com/mirrors/react.git
  1. Installation dependency
cd react
yarn
  1. Execute the package command to package react, react DOM, and scheduler into files separately for easy reading (JDK needs to be installed in the build process)
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE

tips: if you have difficulty build ing by yourself, you can directly use my packaged

Packaged address

  1. Create your own react project using cra
npx create-react-app my-app
  1. Use our packaged js file in my app project
step1: It's packed react Execute under directory yarn link
step2: stay my-app Execute under project directory yarn link react

step3: It's packed react-dom Execute under directory yarn link
step4: stay my-app Execute under project directory yarn link react-dom
  1. Test whether the yarn link is effective
// In react DOM / CJS / react dom development. Add your own log to JS

// 1. the entrance function that is called in the application is here.
function render(element, container, callback) {
  console.log('react render Function executed');
  if (!isValidContainer(container)) {
    {
      throw Error( "Target container is not a DOM element." );
    }
  }

  {
    var isModernRoot = isContainerMarkedAsRoot(container) && container._reactRootContainer === undefined;

    if (isModernRoot) {
      error('You are calling ReactDOM.render() on a container that was previously ' + 'passed to ReactDOM.createRoot(). This is not supported. ' + 'Did you mean to call root.render(element)?');
    }
  }

  return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}

Start my react project and open the console... You can see that react and react DOM are packaged instead of node_modules. Then we can debug happily!

Fiber architecture concept of React

The latest react architecture can be divided into three layers:

  • Scheduler -- the priority of scheduling tasks, and high priority tasks enter the Reconciler first
  • Reconciler -- responsible for comparing the changes of nodes before and after update (diff algorithm)
  • Renderer -- responsible for rendering nodes that need to be changed to the page

Scheduler

The scheduler package has only one core responsibility, which is to execute callback

  • Wrap the callback function provided by react reconciler into a task object
  • Maintain a task queue internally, with high priority at the top
  • Cycle the consumption task queue until the queue is empty

Reconciler

The react reconciler package has three core responsibilities:

  1. Load the renderer, which must be implemented HostConfig protocol (e.g. react dom) to ensure that the api of the renderer can be called correctly when necessary to generate actual nodes (e.g. dom nodes)
  2. Receive update requests initiated by react DOM package (initial render) and react package (subsequent update of setState)
  3. Wrap the construction process of fiber tree in a callback function, and pass this callback function into the scheduler package to wait for scheduling

The most well-known in react is interruptible rendering. Why unsafe after react16_ componentWillMount, UNSAFE_ The declaration cycle function executed by the componentwillreceiveproprender phase is unsafe because the render phase is interruptible. But! Only in hostrootfiber Mode = = = concurrent root | blockingroot will be enabled. In case of legacy mode, it is through reactdom When render (< app / >, DOM) is started in this way, in this case, both the first render and subsequent update s will only enter the synchronization work cycle, and the reconciliation will not be interrupted, so the lifecycle function will only be called once. Therefore, although interruptible rendering has been implemented in react17, so far, it is still Experimental function.

Renderer

The react DOM package has two core responsibilities:

  1. Guide the startup of react application (through ReactDOM.render)
  2. realization HostConfig protocol(The source code is in reactdomhostconfig JS ), it can express the fiber tree constructed by react reconciler package, generate dom node (in browser), and generate string (ssr)

How React's Fiber architecture works

Dual cache Fiber tree

What is called dual cache: the technology of building and directly replacing in memory is called dual cache.

There are at most two Fiber trees in React at the same time The Fiber tree corresponding to the content displayed on the current screen is called current Fiber tree, and the Fiber tree being built in memory is called workInProgress Fiber tree.

The Fiber node in the current Fiber tree is called current Fiber, and the Fiber node in the workinprogress Fiber tree is called workinprogress Fiber. They are connected through the alternate attribute.

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

The root node of React application completes the replacement of current tree and workInProgress tree by changing the direction of current pointer in different Fiber trees.

After the construction of the workInProgress tree is completed and handed over to the Renderer for rendering on the page, the fiberrootnode pointer points to the workInProgress Fiber tree. At this time, the workInProgress tree becomes the current Fuber tree

Each status update will generate a new workInProgress Fiber tree. The DOM update is completed through the replacement of current and workInProgress.

mount time

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 1
    }
  }

  render() {
    const { number } = this.state;
    return (
      <div onClick={() => {this.setState({number: number+1})}}>
         classComponent: { number}
      </div>
    )
  }
}


ReactDOM.render(
  <App />,
  document.getElementById('root')
)
  1. Execute reactdom for the first time Render will create fiberRootNode (called fiberroot in the source code) and rootFiber. Where fiberRootNode is the root node of the whole application, and rootFiber is the root node of the component where < app / >.

Because in our application, reactdom can be called many times render. Then there will be multiple rootfibers, but there is only one fiberRootNode. The fiberRootNode always points to the Fiber tree rendered on the current page, that is, the current Fiber tree

fiberRootNode.current = rootFiber
  1. Next, enter the render stage. According to the JSX returned by the component, create Fiber nodes in memory and connect them together to build a Fiber tree, which is called workInProgress Fiber tree. (in the following figure, the tree built in memory is on the right, and the tree displayed on the page is on the left)

When building the workInProgress Fiber tree, you will try to reuse the attributes in the existing Fiber node in the current Fiber tree. During the first screen rendering, only rootFiber has the corresponding current Fiber (i.e. rootFiber.alternate).

  1. The workInProgress Fiber tree built on the right in the above figure is rendered to the page in the commit phase. At this time, the DOM is updated to the corresponding appearance on the right. The current pointer of the fiberRootNode points to the workInProgress Fiber tree to make it a current tree

Corresponding code:

// Focus on this line of code in the commitRootImpl function!!! Switching between workInProgress tree and current tree
root.current = finishedWork;

update

  1. Next, we call setState to trigger the update. This time, a new render phase will be started and a new workInProgress tree will be built

As in mount, the creation of workInProgress fiber can reuse the node data corresponding to the current Fiber tree.

The process of deciding whether to reuse is Diff algorithm

  1. After the construction of workInProgress Fiber tree is completed in the render phase, it enters the commit phase and is rendered on the page. After rendering, the workInProgress Fiber tree becomes the current Fiber tree.

High frequency object interpretation in React

Fiber object

Let's first look at the data structure. Its type type is defined in ReactInternalTypes.js Medium:

export type Fiber = {|
  tag: WorkTag, // Identify the fiber type and generate it according to the type of ReactElemnt component
  key: null | string, // The unique representation of this node is used for the optimization of diff algorithm
  elementType: any, // Generally speaking, it is consistent with the type of ReactElement component
  type: any, // Generally speaking, it is similar to fiber ElementType is consistent In some special cases, for example, in the development environment, in order to be compatible with hot reloading, reactelements of function, class and forwardref will be processed to some extent, which will be different from fiber elementType
  stateNode: any, // Local state node associated with fiber (for example: HostComponent type points to dom node corresponding to fiber node; root node fiber.stateNode points to FiberRoot; class type node whose stateNode points to class instance)
  
  return: Fiber | null, // Parent node of this node
  child: Fiber | null, // The first child node of this node
  sibling: Fiber | null, // The next child node of this node
  index: number, // Subscript of this node
  ref:
    | null
    | (((handle: mixed) => void) & { _stringRef: ?string, ... })
    | RefObject,
  pendingProps: any, // props passed in from 'ReactElement' object For and ` fiber Memoizedprops ` whether the attribute changes can be obtained by comparison
  memoizedProps: any, // The attributes used in the last generation of child nodes are kept in memory after the generation of child nodes
  updateQueue: mixed, // The queue that stores state updates. After the state of the current node is changed, an update object will be created and added to the queue
  memoizedState: any, // The state used for output and the state used for final rendering
  dependencies: Dependencies | null, // The (contexts, events) that the fiber node depends on
  mode: TypeOfMode, // The binary bit Bitfield is inherited from the parent node and affects all nodes in this fiber node and its subtree It is related to the operation mode of react application (with options such as concurrent mode, blockingmode, nomode, etc.)

  // Effect is related to side effects
  flags: Flags, // Flag bit
  subtreeFlags: Flags, //Alternative 16 Firsteffect and nexteffect in version X It is enabled only when enableNewReconciler=true is set
  deletions: Array<Fiber> | null, // Stores the child nodes to be deleted It is enabled only when enableNewReconciler=true is set

  nextEffect: Fiber | null, // Unidirectional linked list, pointing to the next fiber node with side effects
  firstEffect: Fiber | null, // Point to the first fiber node in the side effects list
  lastEffect: Fiber | null, // Point to the last fiber node in the side effects list

  // Priority related
  lanes: Lanes, // Priority of this fiber node
  childLanes: Lanes, // Priority of child nodes
  alternate: Fiber | null, // Point to another fiber in memory. Each updated fiber node appears in pairs in memory (current and workInProgress)

  // Related to performance statistics (statistics will be made only after enableProfilerTimer is enabled)
  // The react dev tool evaluates performance based on these time statistics
  actualDuration?: number, // The total time consumed by this node and subtree in this update process
  actualStartTime?: number, // Mark the time when this fiber node starts to build
  selfBaseDuration?: number, // Used for the implementation consumed by the last generation of this fiber node
  treeBaseDuration?: number, // The sum of the time taken to generate the subtree
|};

Update and UpdateQueue objects

When the react program triggers the status update, we will first create an update object.

Process of status Update: trigger status Update - > create Update object - > from fiber to root (markupdatelanefromfibertoroot) - > schedule Update (ensureroot is scheduled) - > render stage (performSyncWorkOnRoot or performcurrentworkonroot) - > commit stage (commitRoot)

export type Update<State> = {|
  eventTime: number, // The time when the update event was initiated (as a temporary field in 17.0.1, it will be moved out soon)
  lane: Lane, // Priority of update

  tag: 0 | 1 | 2 | 3, //  There are 4 kinds in total UpdateState,ReplaceState,ForceUpdate,CaptureUpdate
  payload: any, // The payload can be set as a callback function or object (the first parameter in setState) according to the scene
  callback: (() => mixed) | null, // Callback function (second parameter in setState)

  next: Update<State> | null, // Point to the next one in the linked list. Because UpdateQueue is a circular linked list, the last one is update Next points to the first update object
|};

// =============== UpdateQueue ==============
type SharedQueue<State> = {|
  pending: Update<State> | null,
|};

export type UpdateQueue<State> = {|
  baseState: State,
  firstBaseUpdate: Update<State> | null,
  lastBaseUpdate: Update<State> | null,
  shared: SharedQueue<State>,
  effects: Array<Update<State>> | null,
|};

There is an updateQueue in the fiber object, which is a chain queue. The following figure describes the relationship between fiber, updateQueue and update objects

Hook object

The appearance of Hook makes the function function have the ability of state management react@16.8 After the release, the official began to recommend Hook syntax. Official definitions 14 Hook types.

export type Hook = {|
  memoizedState: any, // Memory status, which is used to output to the final fiber tree of the trip
  baseState: any, // Basic state, when hook After the queue is updated, the baseState will also be updated 
  baseQueue: Update<any, any> | null, // The queue will be merged in the auxiliary state
  queue: UpdateQueue<any, any> | null, // Point to an Update queue
  next: Hook | null, // Point to the next Hook object of the function component, so that multiple hooks also form a linked list
|};

// UpdateQueue and Update are to ensure that the Hook object can be updated smoothly, which is the same as fiber UpdateQueue and Update in UpdateQueue are different (and they are in different files)
type Update<S, A> = {|
  lane: Lane,
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null,
  next: Update<S, A>,
  priority?: ReactPriorityLevel,
|};

//UpdateQueue and Update are to ensure that the Hook object can be updated smoothly, which is the same as fiber UpdateQueue and Update in UpdateQueue are different (and they are in different files)
type UpdateQueue<S, A> = {|
  pending: Update<S, A> | null,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
|};

For more detailed high frequency object solutions, please refer to Graphic React

Experience the workflow of React for the first time

Open the browser's performance and we can see the call stack of react framework. The first rendering can be divided into three parts

Click the event to trigger the call stack when the setState is updated

Detailed startup process of React project

ReactDOM.render(
  <App />,
  document.getElementById('root')
)

In the previous working principle of Fiber architecture, we mentioned that fiberRootNode and rootFiber objects will be created during mount. In fact, we also created a ReactDOMRoot object and called its render method to start rendering our react program.

render stage

The main task of render phase is to create Fiber node and build Fiber tree.

The render phase begins with a call to the performSyncWorkOnRoot or performcurrentworkonroot methods. This depends on whether the update is synchronous or asynchronous (since we call the render method when rendering, the next updates are synchronous by default).

// performSyncWorkOnRoot calls this method
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

The work of performnunitofwork can be divided into two parts: "delivery" and "return".

Before building the Fiber tree, I want to use a 'dynasty' story to describe depth first traversal. It is said that when the emperor dies (the current Fiber node is finished), the throne will be passed to the Prince (the first son is the child in Fiber). If there is no prince, it will be passed to his brother (the brother is the sibling in Fiber node). If the brother node cannot be found, he will look up for his father's brother. When the person found is the first emperor, It means there is no successor. The Dynasty will be destroyed (the traversal of the Fiber tree will be over)

"Delivery" stage

First, start from rootFiber and traverse downward depth first. Call the beginWork method for each Fiber node traversed (which will be explained in detail later). This method will create child nodes according to the incoming Fiber node and connect the two Fiber nodes. When traversing the leaf node, it will enter the "return" stage

"Return" stage

In the "return" stage, completeWork will be called to process Fiber nodes. When a Fiber node completes completeWork (which will be explained in detail later), if it has a brother Fiber node (i.e. Fiber. Sibling! = = null), it will enter the "delivery" stage of its brother Fiber.

If there is no brother Fiber, it will enter the "return" stage of the parent Fiber.

The "delivery" and "return" phases are interleaved until "return" to rootFiber. So far, the render phase is over

example
function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));

The render phase executes in turn

1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

In the render phase beginWork

/**
 * @desc beginWork The job of is to pass in the current Fiber node, create a child Fiber node, and mark the corresponding Fiber with effectTag according to the diff algorithm
 * @params current  The Fiber node corresponding to the current component is the Fiber node of the last update, that is, workinprogress alternate
 * @params workInProgress Fiber node corresponding to the current component
 * @params renderLanes Priority related
*/
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
}


Process Overview
  • Current: the Fiber node of the corresponding Fiber node of the current component at the last update, that is, the node pointed to by the fiberRootNode
  • workInProgress: Fiber node corresponding to the current component (common result of jsx and state)
  • renderLanes: priority dependent

In terms of performance, when the React program is running, it is impossible to recreate the Fiber node every time it is re rendered. I believe you have heard of the diff algorithm. Therefore, in beginWork, it is necessary to distinguish whether to render for the first time or update to reduce unnecessary rendering.

As we mentioned earlier, the double cache mechanism is used in React, but the current tree does not exist during the first rendering, which can be used as the basis for us to judge whether it is the first rendering (i.e. current === null).

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // During update: if there may be an optimization path, you can reuse current (that is, the last updated Fiber node)
  if (current !== null) {
    // ... ellipsis

    // Reuse current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount: create different sub Fiber nodes according to different tag s
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ... ellipsis
    case LazyComponent: 
      // ... ellipsis
    case FunctionComponent: 
      // ... ellipsis
    case ClassComponent: 
      // ... ellipsis
    case HostRoot:
      // ... ellipsis
    case HostComponent:
      // ... ellipsis
    case HostText:
      // ... ellipsis
    // ... Omit other types
  }
}
mount time

As can be seen from the above code, when we determine the first rendering through current===null, we need to create different contents according to the tag attribute in Fiber (therefore, the first screen rendering is actually very time-consuming, which is also a problem in single page applications).

update

We can see that when the following conditions are met, didReceiveUpdate === false (that is, the sub Fiber of the previous update can be reused directly without creating a new sub Fiber)

  1. oldProps === newProps && workInProgress. type === current. Type, namely props and fiber Type unchanged
  2. ! Includesomelane (renderlanes, updatelanes), that is, the priority of the current Fiber node is not enough

Take updateFunctionComponent as an example. If we find that the Fiber node can be reused after a series of judgments. Then, there is no need to spend a lot of operations to diff and reuse the existing Fiber nodes directly.

function updateFunctionComponent {
  ...
   if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  } // React DevTools reads this flag.
}
reconcileChildren

It can be seen from the function name that this is the core part of the Reconciler module.

  • For the component of mount, it will create a child Fiber node
  • For the updated component, it will compare the corresponding Fiber node of the current component at the last update (commonly known as Diff algorithm), and generate a new Fiber node (reuse) from the comparison result
/**
 * reconcileChildren Harmonic function
 * Harmonic function is an important logic in the 'updateXXX' function. Its function is to generate child nodes downward and set 'fiber flags`.
 * There is no comparison object for the 'fiber' node when it is created for the first time, so there is no redundant logic when generating child nodes downward. Just create it
 * When comparing and updating, you need to compare the 'ReactElement' object with the 'old fiber' object to judge whether you need to reuse the 'old fiber' object
*/
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // For mount components
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // For update components
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

For diff algorithm, it will not be expanded in detail here. Students who want to have an in-depth understanding can refer to it diff algorithm

Flags (effecttag in react16)

We know that the work of render phase is carried out in memory. When the work is finished, we need to notify the Renderer to perform specific DOM operations. DOM needs to be executed. What kind of DOM operation should be executed? According to fiber flags.

For example:

// DOM needs to be inserted into the page
export const Placement = /*                */ 0b00000000000010;
// DOM needs to be updated
export const Update = /*                   */ 0b00000000000100;
// DOM needs to be inserted into the page and updated
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM needs to be deleted
export const Deletion = /*                 */ 0b00000000001000;
function markUpdate(workInProgress) {
  workInProgress.flags |= Update; // With the tag of Update (effecttag in react16), is it in the beginwork stage or the completeWork stage???
}
updateClassComponent{
  ...
  // Label Placement
  workInProgress.flags |= Placement;
}

In the render phase completeWork

/**
 * After the diff of the previous node is completed, do some finishing work on it.
 * 1. Put the name of the attribute to be updated into the updateQueue attribute of the Fiber node
 * 2. Generate EffectList(subtreeFlags)
*/
function completeWork(current, workInProgress, renderLanes) {
  var newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    ...
  }
}

Process Overview

In beginWrok, we mentioned that after beginWork is executed, a child Fiber node will be created, and there may be an effectTag on the Fiber node.

Similar to beginWork, completeWork is also for different fibers Tag calls different processing logic.

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ... ellipsis
      return null;
    }
    case HostRoot: {
      // ... ellipsis
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ... ellipsis
      return null;
    }
  // ... ellipsis

We focus on the HostComponent necessary for page rendering (that is, the Fiber node corresponding to the native DOM component).

Process HostComponent

Like beginWork, we use current === null? Determine whether it is mount or update.

Update time

When updating, the corresponding DOM node already exists for the Fiber node, so there is no need to generate a DOM node. The main thing to do is to deal with props, such as:

  • Registration of callback functions such as onClick and onChange
  • Handling style prop
  • Handling DANGEROUSLY_SET_INNER_HTML prop
  • Dealing with children prop

Within updateHostComponent, the processed props will be assigned to workinprogress Updatequeue and will eventually be rendered on the page in the commit phase.

workInProgress.updateQueue = (updatePayload: any);

Where updatePayload is in the form of array, the value of its even index is the changed prop key, and the value of its odd index is the changed prop value.

Mount time

As you can see, the main logic of mount includes three:

  • Generate the corresponding DOM node for the Fiber node
  • Insert the descendant DOM node into the newly generated DOM node
  • The process of processing props is similar to that of updateHostComponent in update logic

commit phase

The commitRoot method is the starting point of the commit phase. The fiberRootNode will be passed as a parameter.

In rootfiber A one-way linked list effectList of Fiber nodes that need to perform side effects is saved on the firsteffect, and the changed props are saved in the updateQueue of these Fiber nodes.

DOM operations corresponding to these side effects are executed in the commit phase.

In addition, some life cycle hooks (such as componentDidXXX, useEffect) need to be executed in the commit phase.

The main work of the commit phase (i.e. the work flow of the Renderer) is mainly divided into three parts:

  • before mutation phase (before DOM operation)
  • mutation phase (DOM operation)
  • layout phase (after DOM operation)

before mutation stage

The code in the before mutation stage is very short. The whole process is to traverse the effectList and call the commitbeforemulation effects function for processing.

/** 
 * 1. Deal with the autoFocus and blur logic after DOM node rendering / deletion
 * 2. Call getSnapshotBeforeUpdate lifecycle hook
 * 3. Scheduling useEffect
*/
function commitBeforeMutationEffects(root, firstChild) {
  // 1. Process the autoFocus and blur logic after DOM node rendering / deletion
  focusedInstanceHandle = prepareForCommit(root.containerInfo);
  nextEffect = firstChild;
  // Call the getSnapshotBeforeUpdate useEffect lifecycle function
  commitBeforeMutationEffects_begin(); // We no longer need to track the active instance fiber

  var shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;
  return shouldFire;
}
getSnapshotBeforeUpdate

When it comes to the life cycle function getSnapshotBeforeUpdate, we have to think of the new UNSAFE before the componentWillXXX hook_ Prefix. Since the render phase can be interrupted / restarted, these UNSAFE life cycle functions may be executed repeatedly, but the commit phase is synchronous, so the problem of repeated execution will not be encountered.

mutation stage

Similar to the before mutation stage, the mutation stage also traverses the effectList and executes functions. commitMutationEffects is executed here.

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  // Traverse the effectList
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;

    // Reset the text node according to ContentReset effectTag
    if (effectTag & ContentReset) {
      commitResetTextContent(nextEffect);
    }

    // Update ref
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }

    // Process according to the effectTag
    const primaryEffectTag =
      effectTag & (Placement | Update | Deletion | Hydrating);
    switch (primaryEffectTag) {
      // Insert DOM
      case Placement: {
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        break;
      }
      // Insert Dom and update DOM
      case PlacementAndUpdate: {
        // insert
        commitPlacement(nextEffect);

        nextEffect.effectTag &= ~Placement;

        // to update
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // SSR
      case Hydrating: {
        nextEffect.effectTag &= ~Hydrating;
        break;
      }
      // SSR
      case HydratingAndUpdate: {
        nextEffect.effectTag &= ~Hydrating;

        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // Update DOM
      case Update: {
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // Delete DOM
      case Deletion: {
        commitDeletion(root, nextEffect, renderPriorityLevel);
        break;
      }
    }

    nextEffect = nextEffect.nextEffect;
  }
}

commitMutationEffects will traverse the effectList and perform the following three operations for each Fiber node:

  1. Reset the text node according to ContentReset effectTag
  2. Update ref
  3. Process according to the effectTag respectively, and the effectTag includes (placement | update | deletion | hydration)
Placement effect

When the Fiber node contains Placement effectTag, it means that the DOM node corresponding to the Fiber node needs to be inserted into the page.

The method called is commitPlacement

Let's take a look at the native DOM operation finally invoked

function appendChildToContainer(container, child) {
  var parentNode;

  if (container.nodeType === COMMENT_NODE) {
    parentNode = container.parentNode;
    parentNode.insertBefore(child, container); // Familiar with native DOM operations
  } else {
    parentNode = container;
    parentNode.appendChild(child); // Familiar with native DOM operations
  }
}
Update effect

When the Fiber node contains Update effectTag, it means that the Fiber node needs to be updated. The method called is commitWork, which will be based on Fiber Tag is processed separately.

When fiber If the tag is a FunctionComponent, commitHookEffectListUnmount will be called. This method will traverse the effectList and execute the destruction functions of all uselayouteeffect hooks.

useLayoutEffect(() => {
  // ... Some side effect logic

  return () => {
    // ... This is the destruction function
  }
})

When fiber If the tag is HostComponent, commitUpdate will be called.

Finally, the content corresponding to the updateQueue assigned to the Fiber node in the render stage completeWork (opens new window) will be rendered on the page in updateDOMProperties(opens new window).

for (let i = 0; i < updatePayload.length; i += 2) {
  const propKey = updatePayload[i];
  const propValue = updatePayload[i + 1];

  // Processing style
  if (propKey === STYLE) {
    setValueForStyles(domElement, propValue);
  // Handling DANGEROUSLY_SET_INNER_HTML
  } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
    setInnerHTML(domElement, propValue);
  // Dealing with children
  } else if (propKey === CHILDREN) {
    setTextContent(domElement, propValue);
  } else {
  // Process remaining props
    setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
  }
}
Deletion effect

When the Fiber node contains Deletion effectTag, it means that the DOM node corresponding to the Fiber node needs to be deleted from the page. The method called is commitDeletion.

This method performs the following operations:

  1. Recursively call the Fiber in the Fiber node and its descendants Tag is the component will unmount (opens new window) life cycle hook of ClassComponent. Remove the DOM node corresponding to the Fiber node from the page
  2. Unbind ref
  3. Scheduling the destruction function of useEffect

layout stage

This stage is called layout because the code in this stage is executed after the DOM rendering is completed.

Similar to the first two phases, the layout phase also traverses the effectList and executes functions.

The function to be executed is commitLayoutEffects. commitLayoutEffects mainly does two things

  1. commitLayoutEffectOnFiber (call life cycle hook and hook related operations)
  2. Committatchref (assignment ref)
root.current = finishedWork;

nextEffect = firstEffect;
do {
  try {
    commitLayoutEffects(root, lanes);
  } catch (error) {
    invariant(nextEffect !== null, "Should be working on an effect.");
    captureCommitPhaseError(nextEffect, error);
    nextEffect = nextEffect.nextEffect;
  }
} while (nextEffect !== null);

nextEffect = null;
commitLayoutEffectOnFiber

The commitLayoutEffectOnFiber method will be based on fiber Tag handles different types of nodes separately.

For classComponent, it will call componentDidMount or componentDidUpdate according to mount or update

This. That triggers status updates Setstate if the callback function is assigned the second parameter, it will also be called at this time.

For FunctionComponent and related types, it will call the callback function of uselayouteeffect hook to schedule the destruction and callback function of useEffect

For HostRoot, that is, rootFiber, if the callback function with the third parameter is assigned, it will also be called at this time.

ReactDOM.render(<App />, document.querySelector("#root"), function() {
  console.log("i am mount~");
});
commitAttachRef

The second thing commitLayoutEffects will do is committatchref.

The code logic is simple: get the DOM instance and update the ref.

current Fiber tree switching

We mentioned the dual cache technology in React before. The workInProgress Fiber tree will become the current Fiber tree after rendering in the commit phase. The function of this line of code is to switch the current Fiber tree pointed to by the fiberRootNode.

My React source code comments

For reference only

Problems solved after reading the React source code

Because of the bug caused by using array subscript as key

// It's probably like this, because the array subscript is used as the key
// react is in diff when judging componenet If the type and key are the same, the previous node will be reused. At most, it is an Update. In this way, the initial values in componentDidMount/useState are invalid
list.map((item, key) => (
  <Component key={key} dataSource={item}/>
))

Q&A

Reference link

Uncover the secrets of React Technology

Graphic React principle

react17 source code analysis

react17 source code training camp

Topics: React