react source code analysis 17 context
Video Explanation (efficient learning): Enter learning
Previous articles:
1. Introduction and interview questions
3.react source code architecture
4. Source directory structure and debugging
6.legacy and concurrent mode entry functions
20. Summary & answers to interview questions in Chapter 1
View video debugging demo_ seven
context flowchart
cursor/valueStack
In the react source code, there is a valueStack and valueCursor to record the historical information of the context and the current context, and a didperformworkstack cursor to indicate whether the current context has changed
//ReactFiberNewContext.new.js const valueCursor: StackCursor<mixed> = createCursor(null);
const didPerformWorkStackCursor: StackCursor<boolean> = createCursor(false);
//ReactFiberStack.new.js const valueStack: Array<any> = [];
function pushProvider(providerFiber, nextValue) { var context = providerFiber.type._context; { push(valueCursor, context._currentValue, providerFiber); context._currentValue = nextValue; } }
function popProvider(providerFiber) { var currentValue = valueCursor.current; pop(valueCursor, providerFiber); var context = providerFiber.type._context; { context._currentValue = currentValue; } }
- When the updateContextProvider is called in the render phase, the pushProvider will be executed to push the new value into the valueStack
- When calling completeWork in the commit phase, the popProvider will be executed to pop the context at the top of the stack,
Why is there such a mechanism? Because our context is cross-level. When we talked about the render stage and commit stage earlier, we will traverse the nodes in the way of depth first traversal. If it involves cross-level state reading, we are a little weak, so we need to pass our props down layer by layer, Therefore, we can use a stack to record our context. In the render stage, pushProvider and the commit stage, popProvider can get the current value according to valueCursor at each specific level
createContext
export function createContext<T>( defaultValue: T, calculateChangedBits: ?(a: T, b: T) => number, ): ReactContext<T> { if (calculateChangedBits === undefined) {//Functions that can be passed in to calculate bit calculateChangedBits = null; } else { //... } const context: ReactContext<T> = { $$typeof: REACT_CONTEXT_TYPE, _calculateChangedBits: calculateChangedBits,//Function to calculate the change of value _currentValue: defaultValue,//value of dom environment _currentValue2: defaultValue,//value of art environment _threadCount: 0, Provider: (null: any), Consumer: (null: any), }; context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context, }; if (__DEV__) { } else { context.Consumer = context; } return context; } //Example const NameChangedBits = 0b01; const AgeChangedBits = 0b10; const AppContext = createContext({}, (prevValue, nextValue) => { let result = 0; if (prevValue.name !== nextValue.name) { result |= NameChangedBits; }; if (prevValue.age !== nextValue.age) { result |= AgeChangedBits; }; return result; });
In the simplified createContext, we can see that the relationship between context, Provider and Consumer is as follows:
context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context, }; context.Consumer = context;
useContext
useContext will call readContext, which will create a dependency and add it to the dependencies list of the current fiber
function readContext(context, observedBits) { { if (lastContextWithAllBitsObserved === context) ; else if (observedBits === false || observedBits === 0) ; else { var resolvedObservedBits; //Generate resolvedObservedBits if (typeof observedBits !== 'number' || observedBits === MAX_SIGNED_31_BIT_INT) { lastContextWithAllBitsObserved = context; resolvedObservedBits = MAX_SIGNED_31_BIT_INT; } else { resolvedObservedBits = observedBits; } var contextItem = {//Generate dependce context: context, observedBits: resolvedObservedBits, next: null }; if (lastContextDependency === null) { //... lastContextDependency = contextItem; currentlyRenderingFiber.dependencies = {//The first of the dependencies linked list lanes: NoLanes, firstContext: contextItem, responders: null }; } else { lastContextDependency = lastContextDependency.next = contextItem;//Add dependencies linked list } } return context._currentValue ; }
provider/customer
In the render phase, updateContextProvider will be called. Pay attention to several key steps
- pushProvider: add the current context to valueStack
- calculateChangedBits: useContext can set observedBits. If it is not set, it is MAX_SIGNED_31_BIT_INT, that is, 31 bit 1, is used to calculate changedBits. The process of calculating whether the context changes takes place in the calculateChangedBits function. In this way, the performance after the context changes can be improved
- Bailoutonalreadyfinished work / propagateContextChange: if the changedBits has not changed, follow the logic of bailoutonalreadyfinished work and skip the update of the current node. If it has changed, execute propagateContextChange
function updateContextProvider(current, workInProgress, renderLanes) { var providerType = workInProgress.type; var context = providerType._context; var newProps = workInProgress.pendingProps; var oldProps = workInProgress.memoizedProps; var newValue = newProps.value; //... pushProvider(workInProgress, newValue); if (oldProps !== null) { var oldValue = oldProps.value; var changedBits = calculateChangedBits(context, newValue, oldValue); if (changedBits === 0) {//The context has not changed if (oldProps.children === newProps.children && !hasContextChanged()) { return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } } else {//context changed propagateContextChange(workInProgress, context, changedBits, renderLanes); } } var newChildren = newProps.children; reconcileChildren(current, workInProgress, newChildren, renderLanes); return workInProgress.child; }
function calculateChangedBits(context, newValue, oldValue) { if (objectIs(oldValue, newValue)) { //No change return 0; } else { var changedBits = typeof context._calculateChangedBits === 'function' ? context._calculateChangedBits(oldValue, newValue) : MAX_SIGNED_31_BIT_INT; { if ((changedBits & MAX_SIGNED_31_BIT_INT) !== changedBits) { error('calculateChangedBits: Expected the return value to be a ' + '31-bit integer. Instead received: %s', changedBits); } } return changedBits | 0; } } //Example const NameChangedBits = 0b01; const AgeChangedBits = 0b10; const AppContext = createContext({}, (prevValue, nextValue) => { let result = 0; if (prevValue.name !== nextValue.name) { result |= NameChangedBits; }; if (prevValue.age !== nextValue.age) { result |= AgeChangedBits; }; return result; });
function propagateContextChange(workInProgress, context, changedBits, renderLanes) { var fiber = workInProgress.child; if (fiber !== null) { fiber.return = workInProgress;//If the fiber does not exist, find the parent node } while (fiber !== null) { var nextFiber = void 0;//Traverse fiber var list = fiber.dependencies; if (list !== null) { nextFiber = fiber.child; var dependency = list.firstContext; while (dependency !== null) {//Traverse the dependencies linked list if (dependency.context === context && (dependency.observedBits & changedBits) !== 0) { //There are changes if (fiber.tag === ClassComponent) { //Create a new update var update = createUpdate(NoTimestamp, pickArbitraryLane(renderLanes)); update.tag = ForceUpdate; enqueueUpdate(fiber, update); } fiber.lanes = mergeLanes(fiber.lanes, renderLanes);//Merge priority var alternate = fiber.alternate; if (alternate !== null) { alternate.lanes = mergeLanes(alternate.lanes, renderLanes); } scheduleWorkOnParentPath(fiber.return, renderLanes); //Update the priority of the ancestor node list.lanes = mergeLanes(list.lanes, renderLanes); break; } dependency = dependency.next; } } //... nextFiber = fiber.sibling; } else { nextFiber = fiber.child; } //... fiber = nextFiber; } }
The key code of updateContextConsumer is as follows: execute prepareToReadContext to judge whether the priority is enough to join the current render, and readContext gets the value of the current context
function updateContextConsumer(current, workInProgress, renderLanes) { var context = workInProgress.type; //... prepareToReadContext(workInProgress, renderLanes); var newValue = readContext(context, newProps.unstable_observedBits); var newChildren; { ReactCurrentOwner$1.current = workInProgress; setIsRendering(true); newChildren = render(newValue); setIsRendering(false); } //... workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, newChildren, renderLanes); return workInProgress.child; }