2 )源码
定位到 packages/react-reconciler/src/ReactFiberBeginWork.js#L1522
在这里,有这个判断 if ( oldProps === newProps && !hasLegacyContextChanged() && updateExpirationTime < renderExpirationTime ) {}
看到有
import {
hasContextChanged as hasLegacyContextChanged
} from './ReactFiberContext';
关注 hasLegacyContextChanged
基于此,定位到 (packages/react-reconciler/src/ReactFiberContext.js#L115)[https://github.com/facebook/react/blob/v16.6.3/packages/react-reconciler/src/ReactFiberContext.js#L115]
// 要去推入 stack 的值的时候,就要去创建这么一个 cursor 来标记不同类型的一个值
function hasContextChanged(): boolean {
return didPerformWorkStackCursor.current;
}
回顾到 context-stack 中
// packages/react-reconciler/src/ReactFiberContext.js#L36
// A cursor to the current merged context object on the stack.
// 用来记录我们当前的我们更新到某一个节点之后,它应该可以拿到的context对应的所有值
let contextStackCursor: StackCursor<Object> = createCursor(emptyContextObject);
// A cursor to a boolean indicating whether the context has changed.
// 代表着我们在更新到某一个节点的时候,它这个context是否有变化这么一个情况
let didPerformWorkStackCursor: StackCursor<boolean> = createCursor(false);
再次回到 ReactFiberBeginWork.js 中的 if ( oldProps === newProps && !hasLegacyContextChanged() && updateExpirationTime < renderExpirationTime ) {}
对于在 beginWork 中的 context 操作,可以看上述判断成立的条件下的代码,即便有符合跳过更新的操作,依然 需要 push 和 pop 操作
进入代码
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
// 如果当前组件是一个 provider 则进行 push 操作
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
}
跟遗留的 contextAPI 有关,通过 legency 标志,如果一个组件能够作为一个 context 的提供者
那么它肯定是一个 ClassComponent
, 因为要通过 getchildcontext 这么一个方法来声明我们子树当中提供了哪些 concontext
最主要的就是来看在classcomponent的更新过程当中,如果它是一个contextprovider,那么它要执行的操作是 pushLegacyContextProvider
进入 isLegacyContextProvider
, 看到它是 isContextProvider
的别名
// 这个 type 就是组件实例,这个 childContextTypes
function isContextProvider(type: Function): boolean {
// 通过判断应用中声明的 class 上面是否有这个属性
const childContextTypes = type.childContextTypes;
return childContextTypes !== null && childContextTypes !== undefined;
}
进入 pushLegacyContextProvider
它是 pushContextProvider
的别名
function pushContextProvider(workInProgress: Fiber): boolean {
const instance = workInProgress.stateNode;
// We push the context as early as possible to ensure stack integrity.
// If the instance does not exist yet, we will push null at first,
// and replace it on the stack later when invalidating the context.
const memoizedMergedChildContext =
(instance && instance.__reactInternalMemoizedMergedChildContext) ||
emptyContextObject;
// Remember the parent context so we can merge with it later.
// Inherit the parent's did-perform-work value to avoid inadvertently blocking updates.
previousContext = contextStackCursor.current; // 获取之前的 context 挂载到全局变量上
push(contextStackCursor, memoizedMergedChildContext, workInProgress);
push(
didPerformWorkStackCursor,
didPerformWorkStackCursor.current,
workInProgress,
);
return true;
}
以上是可以跳出当前组件的更新的一个处理情况
如果我们可以跳出组件的更新,也就是代表着当前这个 classComponent,它的state它的props都应该是没有任何变化的
这个时候, 当然是可以直接使用保存在它上面原始的 context 的对象
如果它是一个需要更新的 classComponent,需要看一下 updateClassComponent 这个更新方法
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps,
renderExpirationTime: ExpirationTime,
) {
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderExpirationTime);
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
if (current !== null) {
// An class component without an instance only mounts if it suspended
// inside a non- concurrent tree, in an inconsistent state. We want to
// tree it like a new mount, even though an empty version of it already
// committed. Disconnect the alternate pointers.
current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
// In the initial pass we might need to construct the instance.
constructClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
}
return finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
}
一进来就调用了 isLegacyContextProvider
方法
pushLegacyContextProvider
接下去, 它调用了一个方法,叫做 prepareToReadContext
这么一个方法
接下去基本上没有跟 context 相关的内容了,这里进入 finishClassComponent
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderExpirationTime: ExpirationTime,
) {
// Refs should update even if shouldComponentUpdate returns false
markRef(current, workInProgress);
const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;
if (!shouldUpdate && !didCaptureError) {
// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
const instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner.current = workInProgress;
let nextChildren;
if (
didCaptureError &&
typeof Component.getDerivedStateFromError !== 'function'
) {
// If we captured an error, but getDerivedStateFrom catch is not defined,
// unmount all the children. componentDidCatch will schedule an update to
// re-render a fallback. This is temporary until we migrate everyone to
// the new API.
// TODO: Warn in a future release.
nextChildren = null;
if (enableProfilerTimer) {
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (__DEV__) {
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = instance.render();
if (
debugRenderPhaseSideEffects ||
(debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode)
) {
instance.render();
}
ReactCurrentFiber.setCurrentPhase(null);
} else {
nextChildren = instance.render();
}
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
if (current !== null && didCaptureError) {
// If we're recovering from an error, reconcile without reusing any of
// the existing children. Conceptually, the normal children and the children
// that are shown on error are two different sets, so we shouldn't reuse
// normal children even if their identities match.
forceUnmountCurrentAndReconcile(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
} else {
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
}
// Memoize state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
workInProgress.memoizedState = instance.state;
// The context might have changed so we need to recalculate it.
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
return workInProgress.child;
}
hasContext
是否是一个 contextProviderinvalidateContextProvider(workInProgress, Component, false);
function invalidateContextProvider(
workInProgress: Fiber,
type: any,
didChange: boolean,
): void {
const instance = workInProgress.stateNode;
invariant(
instance,
'Expected to have an instance by this point. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
// 如果有变化
if (didChange) {
// Merge parent and own context.
// Skip this if we're not updating due to sCU.
// This avoids unnecessarily recomputing memoized values.
const mergedContext = processChildContext(
workInProgress,
type,
previousContext,
);
instance.__reactInternalMemoizedMergedChildContext = mergedContext;
// Replace the old (or empty) context with the new one.
// It is important to unwind the context in the reverse order.
pop(didPerformWorkStackCursor, workInProgress);
pop(contextStackCursor, workInProgress);
// Now push the new context and mark that it has changed.
push(contextStackCursor, mergedContext, workInProgress);
push(didPerformWorkStackCursor, didChange, workInProgress);
} else {
pop(didPerformWorkStackCursor, workInProgress);
push(didPerformWorkStackCursor, didChange, workInProgress);
}
}
processChildContext
计算出新的 context 并挂载到 __reactInternalMemoizedMergedChildContext
processChildContext
看下这个方法function processChildContext(
fiber: Fiber,
type: any,
parentContext: Object,
): Object {
const instance = fiber.stateNode;
const childContextTypes = type.childContextTypes;
// TODO (bvaughn) Replace this behavior with an invariant() in the future.
// It has only been added in Fiber to match the (unintentional) behavior in Stack.
// 这个属性一定是 function 才能生效
if (typeof instance.getChildContext !== 'function') {
if (__DEV__) {
const componentName = getComponentName(type) || 'Unknown';
if (!warnedAboutMissingGetChildContext[componentName]) {
warnedAboutMissingGetChildContext[componentName] = true;
warningWithoutStack(
false,
'%s.childContextTypes is specified but there is no getChildContext() method ' +
'on the instance. You can either define getChildContext() on %s or remove ' +
'childContextTypes from it.',
componentName,
componentName,
);
}
}
return parentContext;
}
let childContext;
if (__DEV__) {
ReactCurrentFiber.setCurrentPhase('getChildContext');
}
startPhaseTimer(fiber, 'getChildContext');
childContext = instance.getChildContext(); // 执行这个 提供的api, 获取数据
stopPhaseTimer();
if (__DEV__) {
ReactCurrentFiber.setCurrentPhase(null);
}
for (let contextKey in childContext) {
invariant(
contextKey in childContextTypes,
'%s.getChildContext(): key "%s" is not defined in childContextTypes.',
getComponentName(type) || 'Unknown',
contextKey,
);
}
// 忽略
if (__DEV__) {
const name = getComponentName(type) || 'Unknown';
checkPropTypes(
childContextTypes,
childContext,
'child context',
name,
// In practice, there is one case in which we won't get a stack. It's when
// somebody calls unstable_renderSubtreeIntoContainer() and we process
// context from the parent component instance. The stack will be missing
// because it's outside of the reconciliation, and so the pointer has not
// been set. This is rare and doesn't matter. We'll also remove that API.
ReactCurrentFiber.getCurrentFiberStackInDev,
);
}
// 最终是两者 merge
return {...parentContext, ...childContext};
}
contextStackCursor.current
总结来说,父子孙三个组件,在更新子组件的时候,先去push了一个它之前存在的这个属性
因为我们不知道这个组件它是否要更新,不管它是否要更新,都要先都要执行 push 的一个操作
所以,先 push 一个老的值进去再说, 然后到后面,如果发现这个组件它是要更新的,就调用这个 invalidateContextProvider
方法
调用了这个方法之后, 根据传进来的 didChange
,如果是 true 表示要更新,要重新去计算一个新的合并过的这个context, 即 mergedContext
给它推入到栈里面
对于子组件来说,它的所有子树所获取到的context肯定是经过子组件,和上层的父组件合并的 context 了, 也就是 contextStackCursor
这里
同时对于 didPerformWorkStackCursor
来说,因为 didChange
是 true,它的 current 肯定也是 true
如果 didChange
是 false,这个时候不需要改变 contextStackCursor
__reactInternalMemoizedMergedChildContext
这上面的值didPerformWorkStackCursor
来说,需要去改变它didPerformWorkStackCursor
它的一个作用老的 context api当中的 push 操作是比较复杂的,要进行一个 context 的合并这么一个过程
到这里为止,将context的合并,并让它入栈
注意,还有一种情况是这样的,父子孙三层组件,有一个子组件没有儿子组件
也就是有多个子组件,其中有的子组件没有下层组件,这时候这类子组件拿到的是父组件原来的,而非合并过的
看下具体的代码处理, 比如说我们随便挑一个 class component,更新过程当中调用的方法, 如 updateClassInstance
这个方法
// Invokes the update life-cycles and returns false if it shouldn't rerender.
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
ctor: any,
newProps: any,
renderExpirationTime: ExpirationTime,
): boolean {
const instance = workInProgress.stateNode;
// ... 跳过很多代码
const contextType = ctor.contextType; // 注意这里
let nextContext;
if (typeof contextType === 'object' && contextType !== null) {
nextContext = readContext(contextType);
} else {
// 注意这个 else, 这个是重点
const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
nextContext = getMaskedContext(workInProgress, nextUnmaskedContext);
}
// ... 跳过很多代码
return shouldUpdate;
}
这里有一个在 ctor.contextType
在 classComponent 上面去读取这个属性
注意,这里 contextType
和 contextTypes
的区别
Child.contextType = Consumer
对于react来说,它即将把所有context相关的东西呢都放在新的contextAPI里面
getUnmaskedContext(workInProgress, ctor, true)
function getUnmaskedContext(
workInProgress: Fiber,
Component: Function,
didPushOwnContextIfProvider: boolean,
): Object {
if (didPushOwnContextIfProvider && isContextProvider(Component)) {
// If the fiber is a context provider itself, when we read its context
// we may have already pushed its own child context on the stack. A context
// provider should not "see" its own child context. Therefore we read the
// previous (parent) context instead for a context provider.
return previousContext;
}
return contextStackCursor.current;
}
didPushOwnContextIfProvider
传进来的时候是 truepushContextProvider
,给它赋值的这个值getUnmaskedContext
这个方法getMaskedContext
function getMaskedContext(
workInProgress: Fiber,
unmaskedContext: Object,
): Object {
const type = workInProgress.type;
const contextTypes = type.contextTypes;
if (!contextTypes) {
return emptyContextObject;
}
// Avoid recreating masked context unless unmasked context has changed.
// Failing to do this will result in unnecessary calls to componentWillReceiveProps.
// This may trigger infinite loops if componentWillReceiveProps calls setState.
const instance = workInProgress.stateNode;
if (
instance &&
instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext
) {
return instance.__reactInternalMemoizedMaskedChildContext;
}
// 注意,这里是核心
const context = {};
for (let key in contextTypes) {
context[key] = unmaskedContext[key];
}
if (__DEV__) {
const name = getComponentName(type) || 'Unknown';
checkPropTypes(
contextTypes,
context,
'context',
name,
ReactCurrentFiber.getCurrentFiberStackInDev,
);
}
// Cache unmasked context so we can avoid recreating masked context unless necessary.
// Context is created before the class component is instantiated so check for instance.
if (instance) {
cacheContext(workInProgress, unmaskedContext, context);
}
return context;
}
以上,就是对于一个 Class Component,要使用老的contextAPI, 如何去提供这个context以及它如何去获取这个context的一个过程
以上是 push 操作,那什么时候才会 pop 呢?
在 completeUnitOfWork
的时候,在 packages/react-reconciler/src/ReactFiberCompleteWork.js#L540
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// ... 跳过很多代码
switch (workInProgress.tag) {
// ... 跳过很多代码
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
break;
}
// ... 跳过很多代码
}
// ... 跳过很多代码
}
popLegacyContext
别名是 popContext
// packages/react-reconciler/src/ReactFiberContext.js#L124
function popContext(fiber: Fiber): void {
pop(didPerformWorkStackCursor, fiber);
pop(contextStackCursor, fiber);
}
// packages/react-reconciler/src/ReactFiberContext.js#L215
function pushContextProvider(workInProgress: Fiber): boolean {
// ... 跳过很多代码
push(contextStackCursor, memoizedMergedChildContext, workInProgress);
push(
didPerformWorkStackCursor,
didPerformWorkStackCursor.current,
workInProgress,
);
return true;
}
contextStackCursor
再push的是 didPerformWorkStackCursor
didPerformWorkStackCursor
再pop的是 contextStackCursor
popLegacyContext
这个操作就可以了还是用之前的 Fiber 树来举例子
childContextTypes
contextStackCursor
这个游标