LegacyContext
- 老的 contextAPI 也就是我们使用 childContextTypes 这种声明方式
- 来从父节点为它的子树提供 context 内容的这么一种方式
- 遗留的contextAPI 在 react 17 被彻底移除了,就无法使用了
- 那么为什么要彻底移除这个contextAPI的使用方式呢?
- 因为它对性能的影响会比较的大,它会影响整个子树的一个更新过程
- 它嵌套的context提供者是需要进行一个数据的合并的
- 嵌套组件中,如果父子两者都提供相同的变量,会进行一个合并
- 越接近里层,越会被选择
- 也就是说在孙子消费变量的时候,选择父亲的,舍弃爷爷的
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; }
- 通过声明的这个class给它挂载 childContextTypes 来表示它是一个context的提供者
- 只有声明了这个之后,它才会作为一个context的provider来提供子树上面的context
- 如果不这么声明,即便在class里面提供了 getChildContext 这个方法,还是拿不到对应的context
-
进入
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 effectworkInProgress.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
方法- 就是如果它是一个contextprovider,那么它要进行 push 操作
pushLegacyContextProvider
- 就是如果它是一个contextprovider,那么它要进行 push 操作
-
接下去, 它调用了一个方法,叫做
prepareToReadContext
这么一个方法- 这个方法和新的contextAPI有关,先跳过
-
接下去基本上没有跟 context 相关的内容了,这里进入
finishClassComponent
function finishClassComponent(current: Fiber | null,workInProgress: Fiber,Component: any,shouldUpdate: boolean,hasContext: boolean,renderExpirationTime: ExpirationTime, ) {// Refs should update even if shouldComponentUpdate returns falsemarkRef(current, workInProgress);const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;if (!shouldUpdate && !didCaptureError) {// Context providers should defer to sCU for renderingif (hasContext) {invalidateContextProvider(workInProgress, Component, false);}return bailoutOnAlreadyFinishedWork(current,workInProgress,renderExpirationTime,);}const instance = workInProgress.stateNode;// RerenderReactCurrentOwner.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
是否是一个 contextProvider- 如果是 true, 则执行
invalidateContextProvider(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
- 之后,pop 2次,push 2次 来处理了栈内的顺序
- 进入
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,);}// 最终是两者 mergereturn {...parentContext, ...childContext}; }
- 其实,这个 processChildContext 非常简单,获取 context,合并 context
- 这里的 parentContext 是传入的 previousContext, 这个是上面调用 pushContextProvider 时设置的全局变量
contextStackCursor.current
- 也就是 父组件中 提供的 context 对象,最终都是为了合并
- 通过
-
总结来说,父子孙三个组件,在更新子组件的时候,先去push了一个它之前存在的这个属性
-
因为我们不知道这个组件它是否要更新,不管它是否要更新,都要先都要执行 push 的一个操作
-
所以,先 push 一个老的值进去再说, 然后到后面,如果发现这个组件它是要更新的,就调用这个
invalidateContextProvider
方法 -
调用了这个方法之后, 根据传进来的
didChange
,如果是 true 表示要更新,要重新去计算一个新的合并过的这个context, 即mergedContext
给它推入到栈里面 -
对于子组件来说,它的所有子树所获取到的context肯定是经过子组件,和上层的父组件合并的 context 了, 也就是
contextStackCursor
这里 -
同时对于
didPerformWorkStackCursor
来说,因为didChange
是 true,它的 current 肯定也是 true