directory

After reconcileChildFibers reconcileSingleElement reconcileSingleTextNode reconcileChildFibers reconcileSingleElement reconcileSingleTextNode ReconcileChildrenArray Tool function summary lastPlacedIndexCopy the code

Here we use React 17.0.3 as an example.

A, Fiber

React introduced Fiber starting with 16.

Before Fiber, React made a “big move” when it decided to load or update the component tree. This action includes life cycle calls, diff procedure calculations, DOM tree updates, and so on. This action is large, takes a long time, and it happens synchronously. Once started, it cannot be interrupted. This means you can’t do anything until the mount/update is complete.

Faced with the problem of “single task takes too long”, the solution is to divide a huge task into N small tasks (as shown below).

Each tiny task is called Fiber, and it represents one unit of work, the smallest unit of work that receives dispatch.

Each of the peaks in the figure above and in between means a unit of work (Fiber). Each time the peak is reached, it means that the task has surrendered its occupation of the main thread.

Every time you finish a small task, you pause the main thread to see if there is a higher priority that needs to be addressed. This ensures that the main thread is always doing what it should be doing at the moment.

This new reconciliation is called Fiber Reconciler.

Ii. Implementation process



React React React ReactClick here to see)

There are three stages: mount, update and unmount

mount

When a component instance is created and inserted into the DOM

The lifecycle functions are called in the following order:

  • constructor(): initializes state or method binding
  • static getDerivedStateFromProps(): Triggered before each render, not often used
  • render()Check for changes to this.props and this.state and make different render strategies depending on the return value
  • componentDidMount(): immediately after the component is mounted (inserted into the DOM tree)

update

An update is triggered when a component’s props or state changes

The lifecycle functions are called in the following order:

  • static getDerivedStateFromProps(): same as above
  • shouldComponentUpdate(): Determines whether to render the changes of state or props based on the returned value. This parameter is used for performance optimization
  • render(): same as above
  • getSnapshotBeforeUpdate(): called before the last render output (Commit to the DOM node), the pre-commit phase in the figure, not commonly used
  • componentDidUpdate(): Is invoked immediately after the update is complete

uninstall

The following method is called when a component is removed from the DOM:

ComponentWillUnmount () : Called directly before components are unmounted and destroyed

Three, Dom diff

strategy

The traditional DIFF algorithm with O(n^3) time complexity obviously cannot meet the performance requirements of the framework. Therefore, the React team proposed three hypotheses based on the characteristics of the front-end interface:

  1. The same component has the same DOM structure, and different components have different DOM structures (Component diff)

  2. A group of child nodes at the same level that can be distinguished by a unique ID (Element diff)

  3. In THE DOM structure, node operations across the hierarchy are very few and can be ignored (tree diff).

Reduce the time complexity of the DIff algorithm from O(n^3) to O(n) based on these three assumptions

Update logic

In React, Fiber acts as a virtual node

Here, some attributes are captured

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key; / / key values
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null; / / child nodes
  this.sibling = null; // Sibling nodes
  this.index = 0; // Node subscript.this.alternate = null; . }Copy the code

So let’s see how we update our view, right

Starting from the root node:

  • divThe node finds the node through the child attributediv1
  • div1The node finds the node through the sibing attributeul
  • ulThe node finds the node through the child attributeli
  • liA node is compared with the node information stored in its alternate property. After the comparison is complete, update commit3 is submitted to the node via returnulnode
  • ulA node is compared with the node information stored in its alternate property. After the comparison is complete, commit2 is generated, together withcommit3Along with the return todivnode
  • div1A node is compared with the node information stored in its alternate property. After the comparison is complete, update commit1 is submitted to the node via returndivnode
  • After all updates are retrieved (Commit1-3), they are updated again into the real DOM

Source code analysis

Instead of looking up the code step by step, find the react\ Packages \react- Reconciler \ SRC \ reactChildFibre.new.js file.

reconcileChildFibers

The method is used to judge node types and different harmonic functions are used to deal with nodes for different types

function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any, // New fiber
  lanes: Lanes,
) :Fiber | null {

  // If newChild is a Fragment node with no key set (top-level node)
  // Assigns its child to newChild
  const isUnkeyedTopLevelFragment =
      typeof newChild === 'object'&& newChild ! = =null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
  if (isUnkeyedTopLevelFragment) {
    newChild = newChild.props.children;
  }

  const isObject = typeof newChild === 'object'&& newChild ! = =null;
  if (isObject) {
    // newChild is an object
    switch (newChild.$$typeof) {
      // Single node type
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      // The following case is handled similarly to the single-node type. }if (isArray(newChild)) {
      // Array type
      returnreconcileChildrenArray( returnFiber, currentFirstChild, newChild, lanes, ); }...// Invalid object type
    throwOnInvalidObjectType(returnFiber, newChild);
  }
  // Text node
  if (typeof newChild === 'string' || typeof newChild === 'number') {
    return placeSingleChild(
      reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        ' '+ newChild, lanes, ), ); }...// Update deletes all nodes
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}
Copy the code

reconcileSingleElement

Handle New Fiber as a single node

function reconcileSingleElement(
  returnFiber: Fiber, / / workInProgress namely
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
) :Fiber {
  const key = element.key;
  let child = currentFirstChild; // Old fiber
  // If child is not null, loop through
  while(child ! = =null) {
    if (child.key === key) {
      // The key value is the same
      const elementType = element.type;
      if (elementType === REACT_FRAGMENT_TYPE) {
        if (child.tag === Fragment) {
          // Same type, can reuse Old fiber, delete sibling nodes
          deleteRemainingChildren(returnFiber, child.sibling);
          constexisting = useFiber(child, element.props.children); existing.return = returnFiber; .returnexisting; }}else {
        if (
          child.elementType === elementType ||
          (__DEV__
            ? isCompatibleFamilyForHotReloading(child, element)
            : false) ||
          (enableLazyElements &&
            typeof elementType === 'object'&& elementType ! = =null &&
            elementType.$$typeof === REACT_LAZY_TYPE &&
            resolveLazy(elementType) === child.type)
          ) {
          // Same type, can reuse Old fiber, delete sibling nodes
          deleteRemainingChildren(returnFiber, child.sibling);
          constexisting = useFiber(child, element.props); existing.ref = coerceRef(returnFiber, child, element); existing.return = returnFiber; .returnexisting; }}// Delete the current child
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      Delete the current child because the key value is inconsistent
      deleteChild(returnFiber, child);
    }
    // The current child cannot be reused
    // Use child's sibling node to continue the comparison (optimization point)
    child = child.sibling;
  }
  // If there are no reusable ones, go to create them here
  if (element.type === REACT_FRAGMENT_TYPE) {
    const created = createFiberFromFragment(
      element.props.children,
      returnFiber.mode,
      lanes,
      element.key,
    );
    created.return = returnFiber;
    return created;
  } else {
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    returncreated; }}Copy the code

reconcileSingleTextNode

Next deal with the New fiber of the text node type

function reconcileSingleTextNode(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  textContent: string,
  lanes: Lanes
) :Fiber {
  if(currentFirstChild ! = =null && currentFirstChild.tag === HostText) {
    // Delete sibling nodes of the same type
    deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
    const existing = useFiber(currentFirstChild, textContent);
    existing.return = returnFiber;
    return existing;
  }
  // The type is inconsistent and cannot be reused. Delete nodes and create new nodes
  deleteRemainingChildren(returnFiber, currentFirstChild);
  const created = createFiberFromText(textContent, returnFiber.mode, lanes);
  created.return = returnFiber;
  return created;
}
Copy the code

reconcileChildrenArray

Finally, there is the New Fiber that deals with array types, and this is the part that most articles discuss the most

function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  lanes: Lanes,
) :Fiber | null {...let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;

  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;

  // First traversal
  for(; oldFiber ! = =null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {
      // The old node is to the right of the new node
      // Step 1: In the next loop, the old node remains unchanged and the new node is taken to the right
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      // Step 2: Use the sibling of the old node as the old node for the next iteration (at the end of the loop, assign nextOldFiber to oldFiber)
      nextOldFiber = oldFiber.sibling;
    }
    // Step 3: Check whether nodes can be reused by key
    // If reusable, return the fiber created by the old node
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx],
      lanes,
    );
    if (newFiber === null) {
      // Step 4: Unreusable nodes, out of the loop
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }

    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        // Step 5: Delete the old nodedeleteChild(returnFiber, oldFiber); }}// Step 6: Update operationlastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); .// Step 7: Assign the latter node to oldFiber
    oldFiber = nextOldFiber;
  }
  // The first traversal is complete
  if (newIdx === newChildren.length) {
    // Step 8: Delete the remaining old nodes after all new nodes are traversed
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  if (oldFiber === null) {
    // The old node is traversed
    // Step 9: If there are any new nodes, enter the following loop to create them one by one
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }

  // Step 10: Use Map to save the old node information
  // If the old node has a key value, use the key value as the Map key value
  // If no key value exists, use the subscript as the Map key value
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  // A node that cannot be reused during the first loop will jump out and go through the second loop

  // The second pass
  for (; newIdx < newChildren.length; newIdx++) {
    // Step 11: Check whether existingChildren has the same old node by the key value or index of the new node
    // If not, a new Fiber will be created for the new node
    // More on this in the next section
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );
    if(newFiber ! = =null) {
      if (shouldTrackSideEffects) {
        if(newFiber.alternate ! = =null) {
          // Step 12: If newFiber is an old node for reuse, delete the corresponding node in existingChildren
          existingChildren.delete(
            newFiber.key === null? newIdx : newFiber.key, ); }}// Step 13: Update operationlastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); . }}// Step 14: Clear existingChildren after completing the second walk
  if (shouldTrackSideEffects) {
    existingChildren.forEach(child= > deleteChild(returnFiber, child));
  }
  return resultingFirstChild;
}
Copy the code

Tool function

With the process almost complete, let’s take a look at some important utility functions

Handles the insertion of text nodes in the reconcileChildFibers method
ShouldTrackSideEffects specifies whether to add an effectTag to the Fiber object
// shouldTrackSideEffects is true for update
// The alternate property is null, indicating that the fiber is not yet inserted into the Dom
function placeSingleChild(newFiber: Fiber) :Fiber {
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.effectTag = Placement;
  }
  return newFiber;
}
Copy the code
It is used in the second pass of the reconcileChildrenArray method
// Iterate over the old nodes at the current level to get a Map with key or index as key and node as value
function mapRemainingChildren(returnFiber: Fiber, currentFirstChild: Fiber) :Map<string | number.Fiber> {
  const existingChildren: Map<string | number, Fiber> = new Map(a);let existingChild = currentFirstChild;
  while(existingChild ! = =null) {
    if(existingChild.key ! = =null) {
      existingChildren.set(existingChild.key, existingChild);
    } else {
      existingChildren.set(existingChild.index, existingChild);
    }
    existingChild = existingChild.sibling;
  }
  return existingChildren;
}
Copy the code
It is used in the second pass of the reconcileChildrenArray method
// Check whether existingChildren has the same old node by the key value or index of the new node
function updateFromMap(
  existingChildren: Map<string | number, Fiber>,
  returnFiber: Fiber,
  newIdx: number,
  newChild: any,
  lanes: Lanes
) :Fiber | null {
  if (typeof newChild === "string" || typeof newChild === "number") {
    const matchedFiber = existingChildren.get(newIdx) || null;
    return updateTextNode(returnFiber, matchedFiber, "" + newChild, lanes);
  }
  if (typeof newChild === "object"&& newChild ! = =null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE: {
        const matchedFiber =
          existingChildren.get(newChild.key === null ? newIdx : newChild.key) ||
          null;
        return updateElement(returnFiber, matchedFiber, newChild, lanes);
      }
      case REACT_PORTAL_TYPE: {
        ...
    }
    if (isArray(newChild) || getIteratorFn(newChild)) {
      const matchedFiber = existingChildren.get(newIdx) || null;
      return updateFragment(returnFiber, matchedFiber, newChild, lanes, null); }... }...return null;
}
Copy the code
It is used in both cycles in the reconcileChildrenArray method
function placeChild(newFiber: Fiber, lastPlacedIndex: number, newIndex: number,) :number {
    newFiber.index = newIndex;
    if(! shouldTrackSideEffects) {// No update required
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    if(current ! = =null) {
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        // If the old reusable node is to the left of the new node, move the old reusable node to the right
        newFiber.flags |= Placement;
        return lastPlacedIndex;
      } else {
        / / don't touch it
        returnoldIndex; }}else {
      // Insert a new node
      newFiber.flags |= Placement;
      returnlastPlacedIndex; }}Copy the code

summary

  • If the New fiber type is text, check whether the New fiber type and the Old fiber type are text (the text content need not be the same).

    • If yes, delete the sibling node of Old Fiber and reuse Old fiber
    • If not, delete the Old fiber and create a new text node
  • If the New fiber is a single node, check whether the key values are the same

    • If yes, check whether the node types are the same
      • If yes, delete the sibling nodes of the Old fiber and reuse the Old fiber
      • If no, delete the current Old fiber and use the sibling node of the current Old fiber to determine the key value and type again
    • If no, delete the current Old fiber and use the sibling node of the current Old Fiber to determine the key value and type again
  • If New Fiber is array type (multiple nodes exist)

    • The first time through, compareThe same locationTo check whether the key values of the old and new nodes are the same
      • If yes, the current node can be reused
      • Inconsistent, then out of the loop
    • The first traversal is complete
      • If the new node is traversed and the old node is still available, delete the old node and return the result to end the reconciliation process
      • If the old node is traversed and the new node is left, the new node is traversed to create a new Fiber and inserted, and the result is returned to end the reconciliation process
      • If both the Old and new nodes are left, i.e. the first iteration is out of the loop, then the second iteration is started by traversing the Old fiber to get a Map with the Key value or index as the Key value and the Old node as the value
    • For the second traversal, check whether there are nodes with the same key value in Old Fiber according to the Map obtained in the previous step
      • If yes, remove the old node, reuse the old node, insert the node, and then delete the corresponding old node in the Map
      • If no, create (inupdateFromMapIn the implementation)

lastPlacedIndex

Finally, let’s see what the last place index through diff actually does.

LastPlacedIndex records the position of the last node. During the update operation, compare the index of the node to be updated with that of lastPlacedIndex. Move the node to the right only when the index is smaller than lastPlacedIndex.

Vue’s diff algorithm allows nodes to be moved left and right, while React’s policy allows nodes to be moved right only



I love learning


The article is also posted on my official account, welcome to follow MelonField

reference

  • Github.com/facebook/re…
  • Zh-hans.reactjs.org/docs/react-…
  • Blog.csdn.net/susuzhe123/…
  • www.jianshu.com/p/6d7411c85…