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 in
context._currentValue
(Also save tocontext._currentValue2
Save 2 values to support concurrent rendering by multiple renderers. - Also created
context.Provider
.context.Consumer
2reactElement
Object.
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:
-
Use the MyContext.Consumer component: for JSX. Such as < MyContext. Consumer > (value) = > {} < / MyContext Consumer >
beginWork
forContextConsumer
Type, 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
-
Use useContext: for function. For example, const value = useContext(MyContext)
- Enter the
updateFunctionComponent
After, the call will be madeprepareToReadContext
- Whether it’s the first timeCreate a stage, orUpdate the stage.
useContext
It’s all called directlyreadeContext
- Enter the
-
In the class component, use a static contextType property: used to get the context from the class component. For example, myclass.contextType = MyContext;
- Enter the
updateClassComponent
After, the call will be madeprepareToReadContext
- No matterconstructClassInstance.mountClassInstance.updateClassInstanceInternally call
context = readContext((contextType: any));
- Enter the
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:
prepareToReadContext
Set:currentlyRenderingFiber = workInProgress
And resetlastContextDependency
And so on.readContext
Returns thecontext._currentValue
And construct onecontextItem
Added to theworkInProgress.dependencies
After 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:
value
No change, straight inBailout
(Can be recalledFiber tree structure (Comparison update)In thebailout
The explanation).value
Change, 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:
- Traversal down: from
ContextProvider
Type of node start, look down allfiber.dependencies
Rely on thecontext
The node of (suppose calledconsumer
). - Traversal up: from
consumer
Node starts, traverses up, and modifies all nodes on the parent pathfiber.childLanes
Property 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
scheduleWorkOnParentPath
withmarkUpdateLaneFromFiberToRootThe role of similar, specific can be reviewedFiber tree structure (Comparison update)
- This step is done by callingscheduleWorkOnParentPath(fiber.return, renderLanes)The implementation.
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.
- In the consumption state,
ContextConsumer
Node callreadContext(MyContext)
Get the latest status. - When updating the status, the
ContextProvider
The node is responsible for finding allContextConsumer
Node, and sets the consumption node’s parent path for all nodesfiber.childLanes
To ensure that the consumption node can be updated.