React source code analysis – Fiber construction – completeWork

In the previous article, we introduced beginWork. React uses the depth-first traversal algorithm, and the entire fiber construction follows this algorithm.

This also means that completeWork is not performed until all nodes beginWork is completed.

When the next of beginWork is null, it will enter completeWork.

1. completeUnitOfWork

function completeUnitOfWork(unitOfWork) {<!-- -->
  var completedWork = unitOfWork;
  do {<!-- -->
     //...
     next = completeWork(current, completedWork, subtreeRenderLanes);
     //...
     if (returnFiber !== null & amp; & amp; (returnFiber. flags & amp; Incomplete) === NoFlags) {<!-- -->
       if (returnFiber. firstEffect === null) {<!-- -->
          return Fiber.firstEffect = completedWork.firstEffect;
        }
        
        if (completedWork. lastEffect !== null) {<!-- -->
          if (returnFiber. lastEffect !== null) {<!-- -->
              return Fiber.lastEffect.nextEffect = completedWork.firstEffect;
            }

            return Fiber.lastEffect = completedWork.lastEffect;
        }
        
        var flags = completedWork.flags;
        if (flags > PerformedWork) {<!-- -->
          if (returnFiber. lastEffect !== null) {<!-- -->
            return Fiber.lastEffect.nextEffect = completedWork;
          } else {<!-- -->
            return Fiber. firstEffect = completedWork;
          }
          
          return Fiber.lastEffect = completedWork;
        }
     }
     
     var siblingFiber = completedWork. sibling;

     if (siblingFiber !== null) {<!-- -->
        
        workInProgress = siblingFiber;
        return;
     }
     
     completedWork = returnFiber;
     workInProgress = completedWork;
  } while (completedWork !== null);
  
  //...
}

According to the depth-first algorithm, when beginWork completes the last node of one of the sub-trunks, enter completeUnitOfWork. According to the last node, complete the completeWork first, and go up in turn until the adjacent node is found.

The core method is divided into two parts, the first: completeWork, and the second: complete the effect mount, and connect the effects of all nodes (including the effects inside the node) to assemble a circular linked list.

Two. completeWork

function completeWork(current, workInProgress, renderLanes) {<!-- -->
    var newProps = workInProgress.pendingProps;
    switch (workInProgress. tag) {<!-- -->
      case IndeterminateComponent:
      case LazyComponent:
      case SimpleMemoComponent:
      case FunctionComponent:
      case ForwardRef:
      case Fragment:
      case Mode:
      case Profiler:
      case Context Consumer:
      case MemoComponent:
        return null;
      case ClassComponent:
        //...
      case HostRoot:
       //...
        updateHostContainer(workInProgress);
        return null;
      case HostComponent:
        //...
         var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
         appendAllChildren(instance, workInProgress, false, false);
         workInProgress. stateNode = instance;
        //...
     //...
}

The amazing thing here is that updateHostContainer is actually an empty function inside react, maybe the subsequent version will do something. Normal nodes will go into HostComponent.

createInstance

function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {<!-- -->
    var parentNamespace;

    {<!-- -->
      // TODO: take namespace into account when validating.
      var hostContextDev = hostContext;
      validateDOMNesting(type, null, hostContextDev. ancestorInfo);

      if (typeof props.children === 'string' || typeof props.children === 'number') {<!-- -->
        var string = '' + props.children;
        var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);
        validateDOMNesting(null, string, ownAncestorInfo);
      }

      parentNamespace = hostContextDev.namespace;
    }

    var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
    precacheFiberNode(internalInstanceHandle, domElement);
    updateFiberProps(domElement, props);
    return domElement;
  }

createElement

function createElement(type, props, rootContainerElement, parentNamespace) {<!-- -->
  // script tag processing...
  //...
  
  // flow related webComponents processing
  //...
  
  domElement = ownerDocument.createElement(type);
  
  // special treatment for select tag, set multiple and size separately
  
  // Illegal label alarm processing...
  return domElement;
}

At this point, we can see that the dom object corresponding to each fiber node has been created. But it is not inserted into the document stream. So how is the real dom connected to the fiber object?

precacheFiberNode will mount the fiber of the corresponding node under each real dom object, and precache is an attribute of _reactFiber$ + random number.

updateFiberProps will mount the corresponding props children or element objects under each real dom object, with _reactProps$ + random number.

Here comes the question: is the real DOM object corresponding to each fiber a single storage? Or build a total dom tree? How are these related?

appendAllChildren

appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {<!-- -->
    var node = workInProgress. child;

      while (node !== null) {<!-- -->
        if (node.tag === HostComponent || node.tag === HostText) {<!-- -->
          appendInitialChild(parent, node. stateNode);
        } else if (node. tag === HostPortal) ; else if (node. child !== null) {<!-- -->
          node.child.return = node;
          node = node.child;
          continue;
        }

        if (node === workInProgress) {<!-- -->
          return;
        }

        while (node. sibling === null) {<!-- -->
          if (node.return === null || node.return === workInProgress) {<!-- -->
            return;
          }

          node = node. return;
        }

        node.sibling.return = node.return;
        node = node.sibling;
      }
}

It is already obvious here, according to the fiber linked list created before, the node node is cycled, and the normal node will call appendInitialChild. That is, use: parentInstance.appendChild(child); At this point, you can see that after the loop ends, the entire DOM node to be inserted is generated (when the page is rendered for the first time).

In addition, it should be noted that the association of dom according to fiber is also carried out at this stage (not dom association fiber)

Finally, how does ref exist, the flags corresponding to the current fiber tree will be ORed with the binary data of Ref. (this is very important)

Three. Effect

React advocates functional programming. In a function component, if there are methods such as useEffect, then react considers it a side effect function component. So how are these side effects organized and at what stage do they operate?

As early as beginWork, the fiber flags are all binary 0 by default. If there are side effects, such as: useEffect, ref, useLayoutEffect, etc., they will be set to Placement for the first time. But why there is a fiber object of the useEffect function component, and the flags are all values above 256?

Let’s take useEffect as an example to find out.

useEffect

function useEffect(create, deps) {<!-- -->
    var dispatcher = resolveDispatcher();
    return dispatcher. useEffect(create, deps);
}

That’s right, the useEffect method definition is just two lines of code.

resolveDispatcher

function resolveDispatcher() {<!-- -->
    var dispatcher = ReactCurrentDispatcher. current;

    if (!(dispatcher !== null)) {<!-- -->
      {<!-- -->
        throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\
1. You might have mismatching versions of React and the renderer (such as React DOM )\
2. You might be breaking the Rules of Hooks\
3. You might have more than one copy of React in the same app\
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem." );
      }
    }

    return dispatcher;
  }

Let’s continue to look at the definition of ReactCurrentDispatcher:

const ReactCurrentDispatcher = {<!-- -->
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

The earliest ReactCurrentDispatcher is in the renderRoot stage. If root or Lane is changed, the original dispatch may be empty or does not exist for the first time, use the current ContextOnlyDispatcher instead.

In the function component beginWork stage, before executing the function component to generate the element object, HooksDispatcherOnMount will be assigned, which is dispatch.

Let’s take a look at HooksDispatcher:

{<!-- -->
      readContext: function (context, observedBits) {<!-- -->
        return readContext(context, observedBits);
      },
      useCallback: function (callback, deps) {<!-- -->
        currentHookNameInDev = 'useCallback';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountCallback(callback, deps);
      },
      useContext: function (context, observedBits) {<!-- -->
        currentHookNameInDev = 'useContext';
        mountHookTypesDev();
        return readContext(context, observedBits);
      },
      useEffect: function (create, deps) {<!-- -->
        currentHookNameInDev = 'useEffect';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountEffect(create, deps);
      },
      useImperativeHandle: function (ref, create, deps) {<!-- -->
        currentHookNameInDev = 'useImperativeHandle';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountImperativeHandle(ref, create, deps);
      },
      useLayoutEffect: function (create, deps) {<!-- -->
        currentHookNameInDev = 'useLayoutEffect';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountLayoutEffect(create, deps);
      },
      useMemo: function (create, deps) {<!-- -->
        currentHookNameInDev = 'useMemo';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

        try {<!-- -->
          return mountMemo(create, deps);
        } finally {<!-- -->
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
      },
      useReducer: function (reducer, initialArg, init) {<!-- -->
        currentHookNameInDev = 'useReducer';
        mountHookTypesDev();
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

        try {<!-- -->
          return mountReducer(reducer, initialArg, init);
        } finally {<!-- -->
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
      },
      useRef: function (initialValue) {<!-- -->
        currentHookNameInDev = 'useRef';
        mountHookTypesDev();
        return mountRef(initialValue);
      },
      useState: function (initialState) {<!-- -->
        currentHookNameInDev = 'useState';
        mountHookTypesDev();
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

        try {<!-- -->
          return mountState(initialState);
        } finally {<!-- -->
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
      },
      useDebugValue: function (value, formatterFn) {<!-- -->
        currentHookNameInDev = 'useDebugValue';
        mountHookTypesDev();
        return mountDebugValue();
      },
      useDeferredValue: function (value) {<!-- -->
        currentHookNameInDev = 'useDeferredValue';
        mountHookTypesDev();
        return mountDeferredValue(value);
      },
      useTransition: function () {<!-- -->
        currentHookNameInDev = 'useTransition';
        mountHookTypesDev();
        return mountTransition();
      },
      useMutableSource: function (source, getSnapshot, subscribe) {<!-- -->
        currentHookNameInDev = 'useMutableSource';
        mountHookTypesDev();
        return mountMutableSource(source, getSnapshot, subscribe);
      },
      useOpaqueIdentifier: function () {<!-- -->
        currentHookNameInDev = 'useOpaqueIdentifier';
        mountHookTypesDev();
        return mountOpaqueIdentifier();
      },
      unstable_isNewReconciler: enableNewReconciler
    };

Among them, useEffect focuses on executing mountEffect(create, deps)

function mountEffect(create, deps) {<!-- -->
    {<!-- -->
      // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
      if ('undefined' !== typeof jest) {<!-- -->
        warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1);
      }
    }

    return mountEffectImpl(Update | Passive, Passive$1, create, deps);
  }

mountEffectImpl

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {<!-- -->
  var hook = mountWorkInProgressHook();
    var nextDeps = deps === undefined ? null : deps;

    currentlyRenderingFiber$1.flags |= fiberFlags;
    hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps);
}

The hook object data structure is as follows:

{<!-- -->
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
};

Here comes the problem:

  1. What is currentlyRenderingFiber? What is the relationship with workInProgressFiber?
  2. How are multiple function components and multiple Hooks in a single function component related?
  3. How is the whole hook and fiber related?
  4. What is the complete hooks data structure?

currentlyRenderingFiber is the fiber object currently in the rendering phase, and workInProgressFiber is assigned as early as in the renderHook initialization phase. So the flags of the current function component are changed here, that is, the function flags with side effects = flags|Update|Passive.

According to the binary bit operation, the root function component library flags = 518, of course, this value is not fixed, because the initial flags value of the beginningWork stage changes. There will be different initial values according to different effects.

pushEffect

function pushEffect(tag, create, destroy, deps) {<!-- -->
    var effect = {<!-- -->
      tag: tag,
      create: create,
      destroy: destroy,
      deps: deps,
      // Circular
      next: null
    };
    var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;

    if (componentUpdateQueue === null) {<!-- -->
      componentUpdateQueue = createFunctionComponentUpdateQueue();
      currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {<!-- -->
      var lastEffect = componentUpdateQueue. lastEffect;

      if (lastEffect === null) {<!-- -->
        componentUpdateQueue.lastEffect = effect.next = effect;
      } else {<!-- -->
        var firstEffect = lastEffect. next;
        lastEffect. next = effect;
        effect.next = firstEffect;
        componentUpdateQueue. lastEffect = effect;
      }
    }

    return effect;
  }

For the value of tag, it is the result of the bitwise OR of HasEffect and Passive, which is actually fixed at 5.

It should be noted that the updateQueue of the function component is different from that of rootFiber, and the data structure and function of ordinary nodes are also different. The updateQueue of the function component is associated with the effect. This is a huge difference from the rootFiber in the render initialization phase.

The above code is very simple. If there are multiple effects in each function component, then these effects will be associated in sequence. The fiberr.updateQueue of this function component corresponds to lastEffect, and next is the next effect, until finally forming a ring connected end to end Linked list structure.

Why is it circular? This will be explained in the subsequent scheduling stage.

Think about another question: here is only the effect construction in a single component, so what is the effect construction in the entire fiber list? What is the order of execution?

Four. rootFiber-Effect

At the end of completedWork, according to the depth-first traversal algorithm, the firstEffect of each node is passed up layer by layer until rootFiber. The lastEffect is also judged layer by layer until the last effect of the upper layer is used as the lastEffect of rootFiber.

Each fiber effect is linked through nextEffect, and the fiber internally links its own effect ring list through updateQueue.

So far, the completeWork stage is completed, and most attributes of the rootFiber and each fiber node have been constructed.

In the next chapter, we will enter the commit stage.

Code words are not easy, please like and follow~