Front knowledge

React has been used for nearly a year. I believe you have many doubts like me during the process. For example, why react some health hooks execute multiple times while others execute safely only once? What is fiber in the React 16 update? Such questions, I also puzzled; So I set out to explore the source code;

As you know, react source code is not general, directly read the react source code, really advise to leave… In the collection of react source data, found that the comparison of all and new information is also very few, by chance a chance to see strange dance company boss according to the react source train of thought their debug made a; In the study of his source, I also privately and he communicated a lot, really listen to your words, ten years of books ah; 2333… Thank you very much for answering my question.

Without further discussion, it is highly recommended that his e-book, source code series articles are based on the latest 16.13.1 parsing; It wasn’t finished, but it was wonderful, and I wanted to read it anyway. (A little more, haha)

This article is based on the latest 16.13.1 analysis, the purpose is to understand the overall source process of the general, will not go into the details of things; Understand the react update process as a whole. This will help you explore the final details of the react source code. If there are any errors, please correct them. Although it is now updated to version 16.13.1, the overall architecture remains the same. Here I recommend a few must-read materials.

  1. Lin Clark – A Cartoon Intro to Fiber – React Conf 2017
  2. This is probably the most popular way to open React Fiber
  3. React Fiber Architecture Deep In React

React16 architecture

Before we learn about the react architecture, we need to understand the browser rendering principle. The mainstream browser refresh rate is 60Hz, that is, the browser refresh rate is 16.6ms every (1000ms / 60Hz). As we know, JS can manipulate the DOM, so JS script execution is in the same thread as browser layout and rendering (rendering thread). In one frame, the browser does the following

  1. JS script Execution
  2. Style layout
  3. When JS execution takes longer than 16.6ms, this refresh does not have time to perform style layout and style drawing. That’s what caused the jam

Prior to the 16th edition, the Act 15 architecture consisted of two layers, Reconciler (Reconciler, without interruption) + Renderer (Renderer, without interruption); That is to say, the coordination phase, synchronous (recursive update) update; This can easily cause JS execution to take longer than 16.6ms, meaning that once the update starts, it can be done without interruption. Will cause a lag, such a user experience is very poor;

The React team found that it was perfectly acceptable for users not to feel the delay in their operations. React changes its architecture because JS takes too long to execute. The Act16 architecture can be divided into three layers:

  1. Scheduler (Interruptible) — Scheduling tasks are prioritized, and high-priority tasks are given priority to enter Reconciler
  2. Reconciler — is responsible for identifying the components of change
  3. Renderer (non-interruptible) — Responsible for rendering changing components onto the page
  • This three-tier architecture has the following advantages in my opinion
  1. Like computer networking protocols, where each layer focuses on one thing (a single responsibility), architectures have relatively long life cycles. The TCP/IP protocol has been around for decades. QAQ
  2. Strong scalability and flexibility; Leaves a lot of potential for low-level abstractions open to developers; (ANTD is an example.)
  3. Once you’re familiar with the React framework, it’s relatively easy to switch to other frameworks because React was one of the first mainstream frameworks to emerge. (You should know what you should know)
  • There are also some very obvious downsides
  1. Increased learning costs, such as the new hook and the upcoming stable Concurrent mode, have certain learning costs;
  2. React doesn’t do a lot of optimizations. For example, frameworks like Vue are optimized at compile time; But that’s the difference between a framework and a library; Because React is always positioned as a library, Dan, the core developer of React, has said that the future development of React will not turn into a framework;

Initialization phase

To understand the React update process, I think the best way is to draw a flow chart with a bit of source code comment; Otherwise in the process of learning the source code will be very chaotic; Let’s look at the react initialization phase.

  1. Reactdom.render, remember when the application was mounted?
  • Application mount time entry
  // ReactDOM.render(<App name="Hello"/>, document.querySelector('#app'));
  const ReactDOM = {
  render(element, container) {
    / / create FiberRoot
    const root = container._reactRootContainer = new ReactRoot(container);
    // The first render does not require batch updates
    DOMRenderer.unbatchedUpdates((a)= > {
      // Call FiberRoot's Render method to start renderingroot.render(element); })}Copy the code
  1. Take a look at the FiberRoot data structure
  • I don’t want to post a whole block of code here, just listing the key attributes
  • Regardless of the data structure in FiberNode, we just need to know that it is recording the state and information of the component (class/ FC/Element)
  • FiberNode. Current = RootFiber; FiberNode.
export default class ReactRoot {
  constructor(container) {
    // RootFiber tag === 3
    this.current = new FiberNode(3.null.null);
    // Initialize the updateQueue of rootFiber
    initializeUpdateQueue(this.current);
    // RootFiber points to FiberRoot
    this.current.stateNode = this;
    // The root DOM node that the application mounts
    this.containerInfo = container;
    // root: fiber has been rendered
    this.finishedWork = null; }}Copy the code
  1. UnbatchedUpdates, which involves a react batch update problem;
  • In React, if I call this.setState multiple times on a classComponent click event
  • Take the initiative tobatchedUpdates, prints 1,2,3
  • Event handlers come with their ownbatchedUpdatesIs equivalent to the effect of using a timer, which outputs 0,0,0
  • React assumes that updates that are triggered in a short period of time are not necessary and will automatically be added to eventsbatchedUpdates
  • Of course, the first update is non-batch, which is why the unbatchedUpdates method is called;
  handleClick = (a)= > {
    / / active ` unbatchedUpdates `
    // setTimeout(() => {
    // this.countNumber()
    // }, 0)

    // There is no 'batchedUpdates' in setTimeout
    setTimeout((a)= > {
      batchedUpdates((a)= > this.countNumber())
    }, 0)

    // The event handler comes with 'batchedUpdates', which is the case above
    // this.countNumber()
  }

  countNumber() {
    const num = this.state.number
    this.setState({
      number: num + 1,})console.log(this.state.number)
    this.setState({
      number: num + 2,})console.log(this.state.number)
    this.setState({
      number: num + 3,})console.log(this.state.number)
  }
Copy the code
  1. Fiberroot.render is then called
  • ExpirationTime expirationTime represents the priority of this update.
  • In 16.7, the smaller the expirationTime is, the higher the priority is.
  • After the update is created, the React scheduling phase is entered.
export default class ReactRoot {
  constructor(container) {
    // TODO...
  }   
  render(element) {
    // RootFiber  
    const current = this.current;
    // Apply for the current creation update time
    const currentTime = DOMRenderer.requestCurrentTimeForUpdate();
    // expirationTime expires, which can represent the priority of this update task;
    // Updates triggered by different events have different priorities
    // Different priority makes Fiber get different expirationTime
    const expirationTime = DOMRenderer.computeExpirationForFiber(currentTime, current);
    // Create an update
    const update = createUpdate(expirationTime);
    // Fiber. Tag = HostRoot, payload = ReactComponents
    update.payload = {element};
    enqueueUpdate(current, update);
    // The first render will go here, the next update will directly create the update object and start scheduling
    returnDOMRenderer.scheduleUpdateOnFiber(current, expirationTime); }}Copy the code

First render update process

As usual, we’ll go straight to the flow chart first and then look at the code and comments according to the flow chart. When reading the React source code, it is quite boring. We need a little patience to solve it slowly.

  1. scheduleUpdateOnFiber
  • We only deal with asynchronous tasks, so you don’t need to check if you have an expirationTime to see if it is asynchronous
// Recurse from current fiber to root, and then work from root
export function scheduleUpdateOnFiber(fiber, expirationTime) {
  // The larger the value, the greater the permission, as opposed to 16.7;
  // bubble up update with expirationTime and childExpirationTime
  // The reason for this is that the highest priority of updates in the entire Fiber tree bubbles to the root node for updates
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  // root == FiberRoot
  if(! root) {return;
  }
  // Start scheduling scheduling
  ensureRootIsScheduled(root);
}
Copy the code
  1. EnsureRootIsScheduled starts scheduling
  • This phase is relatively complex, but overall it does the following:
  • Add root to schedule. Only one scheduled task can exist on root at a time
  • This function is called every time an update is created, so consider the following:
  • 1. There are expired tasks on the root and the tasks need to be ImmediatePriority(synchronous without interruption)
  • Root has a scheduled task but has not yet been executed. Compare the new task with the expirationTime and the priority processing
  • 3. Start the render phase of the task if there is no task on root that already has schedule
function ensureRootIsScheduled(root) {
  // This variable records the expirationTime of fiber that has not been executed
  const lastExpiredTime = root.lastExpiredTime;
  if(lastExpiredTime ! == NoWork) {// ....TODO   
  }
  // Find the update expiration time of root (FiberRoot)
  const expirationTime = getNextRootExpirationTimeToWorkOn(root);
  const existingCallbackNode = root.callbackNode;
  // The update expires when there are no tasks
  if (expirationTime === NoWork) {
    // Another asynchronous task in progress exists and is executed synchronously
    if (existingCallbackNode) {
      root.callbackNode = null;
      root.callbackExpirationTime = NoWork;
      root.callbackPriority = Scheduler.NoPriority;
    }
    return;
  }

  // Deduce task priority from the current time and expirationTime
  const currentTime = requestCurrentTimeForUpdate();
  const priorityLevel = inferPriorityFromExpirationTime(currentTime, expirationTime);

  if (existingCallbackNode) {
    // The schedule root already exists on this root
    const existingCallbackNodePriority = root.callbackPriority;
    const existingCallbackExpirationTime = root.callbackExpirationTime;
    if (existingCallbackExpirationTime === expirationTime && existingCallbackNodePriority >= priorityLevel) {
      // The root has an existing task expirationTime which is the same as the expirationTime generated by the new UDpate
      // This means that they may be the same event that triggered the update
      // If an existing task has a higher priority, you can cancel render for this update
      return;
    }
    // If the new UDPate has a higher priority, cancel the previous schedule and start a new one
    Scheduler.cancelCallback(existingCallbackNode);
  }

  root.callbackExpirationTime = expirationTime;
  root.callbackPriority = priorityLevel;
  // Saves asynchronous tasks currently in progress saved by Scheduler
  let callbackNode;
  // Expiration of any synchronization tasks, like the same, uninterrupted, one breath to complete the update;
  if (expirationTime === Sync) {
    callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else {
    // Normal asynchronous tasks and Concurrent first render here
    callbackNode = Scheduler.scheduleCallback(
      priorityLevel, 
      performConcurrentWorkOnRoot.bind(null, root),
      // Calculate a timeout for a task according to expirationTime
      // timeout affects the task execution priority
      {timeout: expirationTimeToMs(expirationTime) - Scheduler.now()}
    )
  }
  root.callbackNode = callbackNode;
}
Copy the code
  1. performSyncWorkOnRoot
  • This is the entry point to the Render phase of a synchronous task that does not pass through scheduler
  • Note that the Render phase is actually the Reconcile phase, and that’s where the DIff algorithm is done;
function  performSyncWorkOnRoot(root) {
  const lastExpiredTime = root.lastExpiredTime;
  constexpirationTime = lastExpiredTime ! == NoWork ? lastExpiredTime : Sync;// Ignore this function for now
  flushPassiveEffects();
  if(root ! == workInProgressRoot || expirationTime ! == renderExpirationTime) {// Create the WIP tree to create the update. If the WIP tree still exists, the task needs to be interrupted
    prepareFreshStack(root, expirationTime);
  }
  // Update according to the WIP tree
  if (workInProgress) {
    const prevExecutionContext = executionContext;
    executionContext |= RenderContext;
    do {
      // Enter the synchronous workLoop
      workLoopSync();
      break;
    } while (true)
    // Render phase ends, enter commit phase, commit phase cannot be interrupted
    commitRoot(root);
    // Rearrange the schedule so that the expired tasks are not executed again;
    ensureRootIsScheduled(root);
  }
  return null;
}
Copy the code
  1. workLoopSync
  • In synchronous mode, there is no need to consider whether the task needs to be interrupted, which is why the render phase can be synchronized;
function workLoopSync() {
  while(workInProgress) { workInProgress = performUnitOfWork(workInProgress); }}Copy the code
  1. performUnitOfWork
  • Start rendering each cell until the WIP tree is empty, i.e., there are no updates;
function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  // beginWork returns fiber.child, and the absence of next means that depth-first traversal has reached the deepest leaf node of a subtree
  // beginWork is one of the main tasks in the render phase, which mainly does the following:
  // Update state according to update
  // Update props according to update
  // Update the effectTag according to update
  let next = beginWork(current, unitOfWork, renderExpirationTime);
  // beginWork completes fiber diff, can update the momoizedProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if(! next) {// completeUnitOfWork does the following:
    // 1. Generate DOM for fiber generated in the beginWork phase and generate DOM tree
    // let next = completeWork(current, workInProgress);
    // 2. Bubble child Fiber's expirationTime to the parent
    // The parent will have the highest expirationTime priority until the descendant
    // resetChildExpirationTime(workInProgress);
    // 3. Assemble the Christmas tree chain effect list
    next = completeUnitOfWork(unitOfWork);
  }
  return next;
}
Copy the code
  • Let’s look at another diagram of the code, and you’ll see; When the work phase is over, the render phase is over
  1. CommitRoot commit phase
  • The commit phase is relatively simple because it is executed synchronously and cannot be interrupted
function commitRoot(root) {
  const renderPriorityLevel = Scheduler.getCurrentPriorityLevel();
  // Wrap commitRoot, commit using Scheduler scheduling
  Scheduler.runWithPriority(Scheduler.ImmediatePriority, commitRootImp.bind(null, root, renderPriorityLevel));
}

// Commit phase entry, including sub-phases such as:
// before mutation phase: the effect list is iterated, and the hook is triggered before the DOM operation is performed
// mutation stage: effect list is traversed and effect is executed
function commitRootImp(root) {
  do {
    // The syncCallback is stored in an internal array and executed synchronously in flushPassiveEffects
    // Because the callback of syncCallback is performSyncWorkOnRoot, a new passive effect may be generated
    / / so need to traverse until rootWithPendingPassiveEffects is empty
    flushPassiveEffects();
  } while(ReactFiberCommitWorkGlobalVariables.rootWithPendingPassiveEffects ! = =null)

  if(! finishedWork) {return null;
  }

  root.finishedWork = null;
  root.finishedExpirationTime = NoWork;

  // Reset Scheduler correlation
  root.callbackNode = null;
  root.callbackExpirationTime = NoWork;
  root.callbackPriority = Scheduler.NoPriority;

  // In the commit phase, the processing of the task corresponding to the expirationTime corresponding to finishedWork is nearing the end
  // Let's find the next task to tackle
  // There is bubbling logic for childExpirationTime in completeUnitOfWork
  // High-priority expirationTime bubbles to the top of a fiber tree
  // So childExpirationTime represents the expirationTime corresponding to the next highest priority task in the entire Fiber tree
  const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(finishedWork);
  // Update root's firstPendingTime, which represents the expirationTime for the next task to proceed
  markRootFinishedAtTime(root, expirationTime, remainingExpirationTimeBeforeCommit);

  if (root === workInProgressRoot) {
    / / reset workInProgress
    workInProgressRoot = null;
    workInProgress = null;
    renderExpirationTime = NoWork;
  }

  let firstEffect;
  if (root.effectTag) {
    // Since the root node's effect list does not contain its own effect, append the root node to the effect list if it has an effect
    if (finishedWork.lastEffect) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else{ firstEffect = finishedWork; }}else {
    // The root node itself has no effect
    firstEffect = finishedWork.firstEffect;
  }
  let nextEffect;
  if (firstEffect) {
    // before mutation stage
    const prevExecutionContext = executionContext;
    executionContext |= CommitContext;
    nextEffect = firstEffect;    
    do {
      try {
        nextEffect = commitBeforeMutationEffects(nextEffect);
      } catch(e) {
        console.warn('commit before error', e); nextEffect = nextEffect.nextEffect; }}while(nextEffect)

    / / mutation stages
    nextEffect = firstEffect;
    do {
      try {
        nextEffect = commitMutationEffects(root, nextEffect);
      } catch(e) {
        console.warn('commit mutaion error', e); nextEffect = nextEffect.nextEffect; }}while(nextEffect)

    // workInProgress Tree now completes rendering side effects into current Tree
    // This is set after the mutation phase so that current still points to the previous tree when componentWillUnmount is triggered
    root.current = finishedWork;
    
    if (ReactFiberCommitWorkGlobalVariables.rootDoesHavePassiveEffects) {
      // This commit contains passiveEffect
      ReactFiberCommitWorkGlobalVariables.rootDoesHavePassiveEffects = false;
      ReactFiberCommitWorkGlobalVariables.rootWithPendingPassiveEffects = root;
      ReactFiberCommitWorkGlobalVariables.pendingPassiveEffectsExpirationTime = expirationTime;
      ReactFiberCommitWorkGlobalVariables.pendingPassiveEffectsRenderPriority = renderPriorityLevel;
    } else {
      // effectList completed, GC
      nextEffect = firstEffect;
      while (nextEffect) {
        const nextNextEffect = nextEffect.next;
        nextEffect.next = null;
        nextEffect = nextNextEffect;
      }
    }
    executionContext = prevExecutionContext;
  } else {
    / / no effectroot.current = finishedWork; }}Copy the code

Non-first render update process

Content to be continued

conclusion

To be updated