React technology disclosure learning

Posted by mosherben on Thu, 03 Feb 2022 14:50:03 +0100

study Uncover the secrets of React Technology

Render phase - Coordinator - Reconciler work

  • Start at the workflow.syncformoncorrent phase
  • Depends on whether the update is synchronous or asynchronous
// performSyncWorkOnRoot calls this method
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// Performcurrentworkonroot calls this method
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
  • shouldYield: judge whether the browser currently has the remaining time
  • workInProcess: represents the currently created workInProcess fiber
  • performUnitOfWork:
    • Create the next Fiber node and assign it to workInProcess
    • The workInProcess is linked with the created fiber node to form a fiber tree
  • Fiber Reconciler reconstructs Stack Reconciler and realizes interruptible recursion through traversal
  • performUnitOfWork. Work is divided into "delivery" and "return"
  • "Delivery" stage
    • Start from rootFiber and traverse downward depth first Call the beginWork method for each fiber node
    • This method creates a child Fiber node based on the incoming Fiber node and links the two Fiber nodes
    • If there are sibling nodes, create sibling nodes in turn according to the nodes
    • When traversing the leaf node, it will enter the return phase
  • "Return" stage
    • The return node calls the completeWork stage to process the fiber node
    • A fiber node completes the completeWork
      • If it has a sibling Fiber node, it will enter the delivery phase of the sibling node
      • If there is no sibling fiber node, it will enter the homing phase of the parent fiber
  • "Delivery" and "return" will be executed alternately until "return" to rootfiber The work of render phase is completed

Leaf nodes with single text will not execute beginWork/completeWork

beginWork

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ... Omit function body
}
  • Current: the Fiber node corresponding to the current component at the time of last update workInProcess.alternate

  • workInProcess: fiber node corresponding to the current component

  • renderLanes: priority dependent

  • Distinguish between mount and update

    • mount: current === null
    • update: current== null
  • begin is divided into two parts:

    • update: the current node exists and meets certain conditions for reuse Clone current Child to workinprocess child
    • mount: except for fiberRootNode, current === null According to fiber Different tags create different types of sub fiber nodes
  • Update: the following conditions are met. - > didReceiveUpdate = false; Directly reuse the last fiber node

    • oldProps === newProps && workInProcess. type === current. Type unchanged
    • includesSomeLane(renderLanes, updateLanes). The priority of the current node is not enough
  • Mount:

    • Enter the creation logic of different types of Fiber according to different types of tag s
    • Eventually, reconcileChildren will be executed: create a child fiber node according to the fiber node
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) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        // Omit processing
      }
       // Reuse current
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } 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
  }
}
  • reconcileChildren work:
    • mount: create a new child fiber node
    • update: the current fiber node will be compared with the last updated fiber node (diff), and a new fiber node will be generated
    • Finally, a new sub fiber node will be generated and assigned to workinprocess Child, as the return value of this beginWork
    • And as the parameter of workInProcess in the next execution of performinunitofwork
    • reconcileChildFibers will bring the effectTag attribute to the generated Fiber node
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,
    );
  }
}
  • effectTag
    • render(Reconciler) phase works in memory After the work is accepted, notify the Renderer to perform dom operation
    • The operation of dom is saved in effectTag
  • Notify the Renderer to insert the dom corresponding to the Fiber node into the page. Two conditions need to be met
    • fiber.stateNode exists, that is, the fiber node saves the corresponding dom node
    • Placement effectTag exists on Fiber node
  • fiber.stateNode will be completed in completeWork
  • When mount ing, only rootFiber will be assigned placement effecttag The commit phase is inserted at one time

commitWork

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
  • HostComponent: the fiber node corresponding to the native component
  • When updating:
    • The dom node corresponding to the Fiber node already exists. There is no need to generate a dom node. It mainly deals with props
      • Registration of callback functions such as onclick and onchange
      • Handling style prop
      • Handling DANGEROUSLY_SET_INNER_HTML prop
      • Dealing with children prop
    • It mainly calls updateHostComponent
      • Assign the processed props to workinprocess updateQueue
      • And will eventually render on the page in the commit phase (Renderer)
      • workInProgress.updateQueue = (updatePayload: any);
      • updatePayload is an array, the even number is the changed prop key, and the odd number is prop value
  • Mount time
    • Main logic:
      • Add the corresponding dom node for the fiber node
      • Insert the descendant dom node into the newly generated dom node
      • The props process is similar to the updateHostComponent in update
  • The completeWork stage belongs to the return stage function Every time appendAllChildren is called, the generated descendant dom node will be inserted under the currently generated dom node
  • When "returning" to rootFiber, there is already a built dom tree in memory
case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    // update
    updateHostComponent(
      current,
      workInProgress,
      type,
      newProps,
      rootContainerInstance,
    );
  } else {
    // mount
    // ... Omit the logic related to server-side rendering
    const currentHostContext = getHostContext();
    // Create corresponding DOM node for fiber
    const instance = createInstance(
        type,
        newProps,
        rootContainerInstance,
        currentHostContext,
        workInProgress,
      );
    // Insert the descendant DOM node into the newly generated DOM node
    appendAllChildren(instance, workInProgress, false, false);
    // The DOM node is assigned to fiber stateNode
    workInProgress.stateNode = instance;

    // The process of processing props is similar to that of updateHostComponent in update logic
    if (
      finalizeInitialChildren(
        instance,
        type,
        newProps,
        rootContainerInstance,
        currentHostContext,
      )
    ) {
      markUpdate(workInProgress);
    }
  }
  return null;
}
  • effectList
  • All fiber nodes are needed to solve the problem. Find the fiber marked effectTag
  • In the upper level function completeUnitOfWork of completeWork:
  • Every time the completeWork is executed and the fiber node of effectTag exists, it will be saved in the one-way linked list of effectList
  • The first node is saved in fiber firstEffect
  • The last node is saved in fiber lastEffect
  • In the "return" stage, all Fiber nodes of effectTag will be added to this linked list
  • Finally, a rootfiber effectList with firsteffect as the starting point
  • The commit phase only needs to traverse the effectList
                       nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
  • After the render phase is completed

    • In the performSyncWorkRoot function, fiberNode is passed to commitRoot
    • Start the work of the commit phase

Commit phase - Renderer - Renderer works

  • fiberRootNode as a parameter
  • fiberRootNode. A unidirectional linked list of Fiber nodes that need to perform side effects is saved on firsteffect
  • The changed props are saved on the updateQueue of these fiber nodes
  • The dom operations corresponding to these side effects are executed in the commit phase
  • Some declaration cycle hook functions (componentdidxxx) and hook (useeffect) are executed in the commit phase
  • Main work in the commit phase
    • before mutation stage: before dom operation
    • mutation phase: execute dom operation
    • layout phase: after dom operation
  • There is still some work before mutation and after layout
    • Trigger of useEffect
    • Priority dependent reset
    • Unbinding of ref
  • before mutation
    • Variable assignment, status reset
    • Finally, a firsteffect will be assigned, which will be used in the three sub phases of commit
  • After layout
    • useEffect related processing
    • Performance tracking correlation
    • Starting from commit, some life cycle functions, hook

before mutation

  • Traverse the effectList and call the commitBeforeMutationEffects function for processing
// Save the previous priority, execute with the synchronization priority, and restore the previous priority after execution
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);

// Mark the current context as CommitContext as the flag of the commit phase
const prevExecutionContext = executionContext;
executionContext |= CommitContext;

// Processing focus status
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;

// Main function of beforeMutation stage
commitBeforeMutationEffects(finishedWork);

focusedInstanceHandle = null;
  • commitBeforeMutationEffects
    • Deal with the logic of autofocus and blur after DOM node rendering / deletion
    • Call getSnapshotBeforeUpdate lifecycle hook
    • Scheduling useEffect
function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const current = nextEffect.alternate;

    if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
      // ... focus blur
    }

    const effectTag = nextEffect.effectTag;

    // Call getSnapshotBeforeUpdate
    if ((effectTag & Snapshot) !== NoEffect) {
      commitBeforeMutationEffectOnFiber(current, nextEffect);
    }

    // Scheduling useEffect
    if ((effectTag & Passive) !== NoEffect) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        // Call a callback function at a certain priority
        scheduleCallback(NormalSchedulerPriority, () => {
          // Calling useEffect
          flushPassiveEffects();
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}
  • Call getSnapshotBeforeUpdate
    • The task in the render phase may be interrupted and restarted, and the componentWillXXX hook may be triggered multiple times Become unsafe
    • Using the new getSnapshotBeforeUpdate, it is executed synchronously in the commit phase, so it will only be triggered once
  • Asynchronous scheduling
    • The flushpassive effects method obtains the effectList from the global variable rootwithpendingpassive effects
    • The effectList contains the Fiber node that needs to perform side effects
      • Insert dom node (PLACEMENT)
      • Update dom node (UPDATE)
      • Delete dom node (DELETION)
      • When a FunctionComponent contains useEffect or useLayoutEffect, its fiber node will also contain Passive effectTag
    • flushPassiveEffect internally traverses the effectList and executes the effect callback function
    • useEffect is divided into three steps:
      1. before mutation stage scheduling flushPassiveEffects in scheduleCallback
      2. After the layout phase, assign the effectList to rootwidth pending passive effects
      3. scheduleCallBack triggers flushpassive effects, which traverses rootwidth pending passive effects internally
    • reason:
      • After the browser finishes rendering and drawing, the function passed to useEffect will delay the call
      • This makes it applicable to many common side-effect scenarios of browsers, such as setting subscriptions and event handling
      • Therefore, you should not block the browser from updating the screen in the function
      • Prevent browser rendering from being blocked during synchronous execution

mutation stage

  • Similarly, traverse the effectList, execute the function, and commitMutationEffects
nextEffect = firstEffect;
do {
  try {
      commitMutationEffects(root, renderPriorityLevel);
    } catch (error) {
      invariant(nextEffect !== null, 'Should be working on an effect.');
      captureCommitPhaseError(nextEffect, error);
      nextEffect = nextEffect.nextEffect;
    }
} while (nextEffect !== null);
  • commitMutationEffects
    • Reset the text node according to ContentRest effectTag
    • Update ref
    • Process respectively according to the effectTag, where the effectTag includes placement | update | deletion | hydration
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;
  }
}
  • Placement effect: the dom node corresponding to the fiber node needs to be inserted into the page
    1. Get parent dom node
    2. Getting the sibling node of fiber, getHostSibling, is very time-consuming, because the same fiber node contains more than HostComponent*
    3. Determine whether to execute parentnode according to the existence of dom node InsertBefore or parentnode appendChild
  • Update effect: the fiber node needs to be updated. Call commitWork
    • FunctionComponent mutation
      • When fiber If the tag is a FunctionComponent, commitHookEffectListUnmount will be called
      • Traverse the effectList and execute the destruction function of all uselayouteeffect hooks
    • HostComponent mutation
      • Call commitUpdate
      • Finally, in updateDOMProperties, render the content corresponding to the updateQueue assigned to the fiber node in the render stage completeWork on the page
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 effectTag, call commitDeletion
    • Recursively call fiber nodes and descendant fiber nodes Call fiber Componentwillsubcomponent with tag as ClassComponent
    • Remove the dom node corresponding to the fiber node from the page
    • Unbind ref
    • Scheduling the destruction function of useEffect

layout

  • The code is executed after dom rendering is completed
  • The life cycle hooks and hooks triggered by this stage can directly access the changed dom
  • Traverse the effectList and execute the function
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    // Call lifecycle hooks and hooks
    if (effectTag & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }

    // Assignment ref
    if (effectTag & Ref) {
      commitAttachRef(nextEffect);
    }

    nextEffect = nextEffect.nextEffect;
  }
}
  • commitLayoutEffects

    1. commitLayoutEffectOnFiber calls life cycle hook and hook related operations
      • ClassComponent calls ComponentDidMount/ComponentDidUpdate
      • Call this State() second parameter function
      • FunctionComponent calls the uselayouteeffect callback function to schedule the destruction and callback function of useEffect
      • Uselayouteeffect(): from the destruction of the last update to the execution of this update, it is executed synchronously
      • useEffect(): it needs to be scheduled first, and then executed asynchronously after the execution is completed in the Layout phase
    2. Committatchref: assign ref: get dom instance and update dom
  • After the mutation phase is executed and before the layout starts, switch to root current = finishWork

  • Because the lifecycle functions executed by layout and hook need to get new dom information

Topics: React