This is the 16th day of my participation in the August Text Challenge.More challenges in August

Simply put, Context provides a way to directly access the state on an ancestor node, eliminating the need for multiple layers of components to pass props.

For Context usage, please refer to the official documentation directly. This article will analyze the implementation principle of Context from the perspective of fiber tree construction.

To create the Context

Use the React. CreateContext API to create a context object. In createContext, you can see the data structure of the Context object:

export function createContext<T> (defaultValue: T, calculateChangedBits: ? (a: T, b: T) => number,) :ReactContext<T> {
  if (calculateChangedBits === undefined) {
    calculateChangedBits = null;
  }
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    // As a workaround to support multiple concurrent renderers, we categorize
    // some renderers as primary and others as secondary. We only expect
    // there to be two concurrent renderers at most: React Native (primary) and
    // Fabric (secondary); React DOM (primary) and React ART (secondary).
    // Secondary renderers store their context values on separate fields.
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0.Provider: (null: any),
    Consumer: (null: any),
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;
  return context;
}
Copy the code

CreateContext core logic:

  • Its initial value is saved incontext._currentValue(Also save tocontext._currentValue2Save 2 values to support concurrent rendering by multiple renderers.
  • Also createdcontext.Provider.context.Consumer2reactElementObject.

For example, create const MyContext = react.createconText (defaultValue); Provider value={/* some value */}> to declare a ContextProvider component.

In the fiber tree rendering, the beginWork node of ContextProvider type is the updateContextProvider handler:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) :Fiber | null {
  const updateLanes = workInProgress.lanes;
  workInProgress.lanes = NoLanes;
  / /... Omit irrelevant code
  switch (workInProgress.tag) {
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
    case ContextConsumer:
      returnupdateContextConsumer(current, workInProgress, renderLanes); }}function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  / /... Omit irrelevant code
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;

  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;
  // Accept the new value
  const newValue = newProps.value;

  / / update ContextProvider _currentValue
  pushProvider(workInProgress, newValue);

  if(oldProps ! = =null) {
    / /... The logic for updating the context is omitted, as discussed below
  }

  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}
Copy the code

The updateContextProvider() is very simple when fiber is first created, just holding the pendingProps. Value as the latest value of the context, which is then used for consumption.

The context. _currentValue storage

PushProvider (workInProgress, newValue) in updateContextProvider -> pushProvider:

/ /... Omit irrelevant code
export function pushProvider<T> (providerFiber: Fiber, nextValue: T) :void {
  const context: ReactContext<T> = providerFiber.type._context;
  push(valueCursor, context._currentValue, providerFiber);
  context._currentValue = nextValue;
}
Copy the code

PushProvider (context._currentValue = nextValue); pushProvider (context._currentValue = nextValue); pushProvider (context._currentValue = nextValue);

The pushProvider counterpart is popProvider, which also takes advantage of stack properties to pop the stack value back to context._currentValue.

This section focuses on the role of Context Api in fiber tree construction. PushProvider /popProvider implementation is illustrated in the React stack.

The Context of consumption

After using the myContext. Provider component, the value of the context is updated by the ContextProvider type Fiber node during fiber tree construction. How do I read context._currentValue later in the process?

React provides three ways to consume Context:

  1. Use the MyContext.Consumer component: for JSX. Such as < MyContext. Consumer > (value) = > {} < / MyContext Consumer >

    • beginWorkforContextConsumerType, and the corresponding handler isupdateContextConsumer
    function updateContextConsumer(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ) {
      let context: ReactContext<any> = workInProgress.type;
      const newProps = workInProgress.pendingProps;
      const render = newProps.children;
    
      / / read the context
      prepareToReadContext(workInProgress, renderLanes);
      const newValue = readContext(context, newProps.unstable_observedBits);
      let newChildren;
    
      / /... Omit irrelevant code
    }
    Copy the code
  2. Use useContext: for function. For example, const value = useContext(MyContext)

    • Enter theupdateFunctionComponentAfter, the call will be madeprepareToReadContext
    • Whether it’s the first timeCreate a stage, orUpdate the stage.useContextIt’s all called directlyreadeContext
  3. In the class component, use a static contextType property: used to get the context from the class component. For example, myclass.contextType = MyContext;

    • Enter theupdateClassComponentAfter, the call will be madeprepareToReadContext
    • No matterconstructClassInstance.mountClassInstance.updateClassInstanceInternally callcontext = readContext((contextType: any));

React calls prepareToReadContext and readContext(contextType) internally based on apis encapsulated in different usage scenarios.

/ /... Omit irrelevant code
export function prepareToReadContext(workInProgress: Fiber, renderLanes: Lanes,) :void {
  // 1. Set global variables in preparation for readContext
  currentlyRenderingFiber = workInProgress;
  lastContextDependency = null;
  lastContextWithAllBitsObserved = null;

  const dependencies = workInProgress.dependencies;
  if(dependencies ! = =null) {
    const firstContext = dependencies.firstContext;
    if(firstContext ! = =null) {
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
        // Context list has a pending update. Mark that this fiber performed work.
        markWorkInProgressReceivedUpdate();
      }
      // Reset the work-in-progress list
      dependencies.firstContext = null; }}}/ /... Omit irrelevant code
export function readContext<T> (
  context: ReactContext<T>,
  observedBits: void | number | boolean,
) :T {
  const contextItem = {
    context: ((context: any): ReactContext<mixed>),
    observedBits: resolvedObservedBits,
    next: null};/ / 1. Construct a contextItem, join to workInProgress. The dependencies list later
  if (lastContextDependency === null) {
    lastContextDependency = contextItem;
    currentlyRenderingFiber.dependencies = {
      lanes: NoLanes,
      firstContext: contextItem,
      responders: null}; }else {
    lastContextDependency = lastContextDependency.next = contextItem;
  }
  // 2. Return currentValue
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
Copy the code

Core logic:

  1. prepareToReadContextSet:currentlyRenderingFiber = workInProgressAnd resetlastContextDependencyAnd so on.
  2. readContextReturns thecontext._currentValueAnd construct onecontextItemAdded to theworkInProgress.dependenciesAfter the linked list.

Note: the readContext not pure function, it also has some side effects, will change the workInProgress. Dependencies, including contextItem. The context to save the current context of references. The Dependencies property is used during updates to determine whether the value in the ContextProvider is depended on.

After returning context._currentValue, proceed with the fiber tree construction until it is complete.

Update the Context

When you go to the update phase, again go to updateContextConsumer

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;

  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;

  const newValue = newProps.value;

  pushProvider(workInProgress, newValue);

  if(oldProps ! = =null) {
    // The update phase is entered
    const oldValue = oldProps.value;
    // Compare newValue with oldValue
    const changedBits = calculateChangedBits(context, newValue, oldValue);
    if (changedBits === 0) {
      // Value does not change, enter Bailout logic
      if( oldProps.children === newProps.children && ! hasLegacyContextChanged() ) {returnbailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); }}else {
      // value changes to find the corresponding consumers and enable them to be updatedpropagateContextChange(workInProgress, context, changedBits, renderLanes); }}/ /... Omit irrelevant code
}
Copy the code

Core logic:

  1. valueNo change, straight inBailout(Can be recalledFiber tree structure (Comparison update)In thebailoutThe explanation).
  2. valueChange, callpropagateContextChange

propagateContextChange:

export function propagateContextChange(workInProgress: Fiber, context: ReactContext
       
        , changedBits: number, renderLanes: Lanes,
       ) :void {
  let fiber = workInProgress.child;
  if(fiber ! = =null) {
    // Set the return pointer of the child to the work-in-progress fiber.
    fiber.return = workInProgress;
  }
  while(fiber ! = =null) {
    let nextFiber;
    const list = fiber.dependencies;
    if(list ! = =null) {
      nextFiber = fiber.child;
      let dependency = list.firstContext;
      while(dependency ! = =null) {
        // Check the dependency context
        if( dependency.context === context && (dependency.observedBits & changedBits) ! = =0
        ) {
          // If the conditions are met, arrange the scheduling
          if (fiber.tag === ClassComponent) {
            The class component needs to create an Update object and add it to the updateQueue queue
            const update = createUpdate(
              NoTimestamp,
              pickArbitraryLane(renderLanes),
            );
            update.tag = ForceUpdate; // Note that ForceUpdate ensures that the class component executes render
            enqueueUpdate(fiber, update);
          }
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if(alternate ! = =null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          / / up
          scheduleWorkOnParentPath(fiber.return, renderLanes);

          // Mark the priority
          list.lanes = mergeLanes(list.lanes, renderLanes);

          // Exit the search
          break; } dependency = dependency.next; }}/ /... Omit irrelevant code
    / /... Omit irrelevant codefiber = nextFiber; }}Copy the code

PropagateContextChange source code is long, the core logic is as follows:

  1. Traversal down: fromContextProviderType of node start, look down allfiber.dependenciesRely on thecontextThe node of (suppose calledconsumer).
  2. Traversal up: fromconsumerNode starts, traverses up, and modifies all nodes on the parent pathfiber.childLanesProperty indicating that the child node has changed and the child node will enter the update logic.
    • This step is done by callingscheduleWorkOnParentPath(fiber.return, renderLanes)The implementation.
      export function scheduleWorkOnParentPath(
        parent: Fiber | null,
        renderLanes: Lanes,
      ) {
        // Update the child lanes of all the ancestors, including the alternates.
        let node = parent;
        while(node ! = =null) {
          const alternate = node.alternate;
          if(! isSubsetOfLanes(node.childLanes, renderLanes)) { node.childLanes = mergeLanes(node.childLanes, renderLanes);if(alternate ! = =null) { alternate.childLanes = mergeLanes( alternate.childLanes, renderLanes, ); }}else if( alternate ! = =null &&
            !isSubsetOfLanes(alternate.childLanes, renderLanes)
          ) {
            alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
          } else {
            // Neither alternate was updated, which means the rest of the
            // ancestor path already has sufficient priority.
            break; } node = node.return; }}Copy the code
    • scheduleWorkOnParentPathwithmarkUpdateLaneFromFiberToRootThe role of similar, specific can be reviewedFiber tree structure (Comparison update)

Through the above two steps, all child nodes consuming the context are guaranteed to be reconstructed, thus ensuring the consistency of the state and realizing the context update.

conclusion

The implementation of Context is still fairly clear, and there are 2 steps.

  1. In the consumption state,ContextConsumerNode callreadContext(MyContext)Get the latest status.
  2. When updating the status, theContextProviderThe node is responsible for finding allContextConsumerNode, and sets the consumption node’s parent path for all nodesfiber.childLanesTo ensure that the consumption node can be updated.