NewContext
1 )概述
- 新的 context API 是一个组件化的使用方式
- 它就跟写其他的组件一样,像写jsx,通过标签的这种方式来赋值一些props
- 还有去给子节点去拿到这个 conntext 的属性
- context的提供方和订阅方都是独立的
- 在什么地方想要用到这个 context
- 就去声明式的写这个 consumer 就可以了
- 而不需要说在这个子树的渲染过程当中
- 都需要处于这个context的一个环境下面
- 没有什么附带的性能影响
- 主要关注源码中的
updateContextProvider
和updateContextConsumer
- 它们就是通过
createContext
返回的两个组件 - 就是provider和consumer, 它们是一个组件的类型
- 所以它们会有特定的组件的更新方式
- 它们就是通过
- 而新的context-api都在 ReactFiberNewContext.js 中
- 它提供的主要核心的两个api就是
pushProvider
以及popProvider
- 它提供的主要核心的两个api就是
2 ) 源码
定位到 packages/react-reconciler/src/ReactFiberBeginWork.js#L1352
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
// 在这个更新的过程中,去获取了 workingprogress点type
// 这个type就是我们通过createcontext返回的那个provider对象
// 这个对象上面会有一个属性指向 consumer 那个type
const providerType: ReactProviderType<any> = workInProgress.type; // 这里获得的就是 provider 组件
const context: ReactContext<any> = providerType._context; // 这里获得的 context 就是 consumer
// 拿到前后两个 props
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
// 跳过
if (__DEV__) {
const providerPropTypes = workInProgress.type.propTypes;
if (providerPropTypes) {
checkPropTypes(
providerPropTypes,
newProps,
'prop',
'Context.Provider',
ReactCurrentFiber.getCurrentFiberStackInDev,
);
}
}
// 注意这里
pushProvider(workInProgress, newValue);
if (oldProps !== null) {
const oldValue = oldProps.value;
const changedBits = calculateChangedBits(context, newValue, oldValue);
if (changedBits === 0) {
// 没有更新
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
} else {
// 存在更新
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(
workInProgress,
context,
changedBits,
renderExpirationTime,
);
}
}
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
return workInProgress.child;
}
- 关于
const context: ReactContext<any> = providerType._context;
// packages/react/src/ReactContext.js#L53 // 在 createContext 函数内 context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context, };
- 进入
pushProvider
// packages/react-reconciler/src/ReactFiberNewContext.js#L55 export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void { const context: ReactContext<T> = providerFiber.type._context; // isPrimaryRenderer 来自 ReactFiberHostConfig.js // 这个值在 dom环境中是 true if (isPrimaryRenderer) { // 这个 valueCursor 记录的是 当前这个树下面一共经历了几个provider,它对应的值 // consumer 也就是 context 它的 value 去获取,是通过赋制在它上面的这个 _currentValue 来进行一个获取的 push(valueCursor, context._currentValue, providerFiber); // 所以, 这跟最终去获取这个 consumer 上面的 context 的时候,跟这个 valueCursor 是没有任何关系的 context._currentValue = nextValue; if (__DEV__) { warningWithoutStack( context._currentRenderer === undefined || context._currentRenderer === null || context._currentRenderer === rendererSigil, 'Detected multiple renderers concurrently rendering the ' + 'same context provider. This is currently unsupported.', ); context._currentRenderer = rendererSigil; } } else { push(valueCursor, context._currentValue2, providerFiber); context._currentValue2 = nextValue; if (__DEV__) { warningWithoutStack( context._currentRenderer2 === undefined || context._currentRenderer2 === null || context._currentRenderer2 === rendererSigil, 'Detected multiple renderers concurrently rendering the ' + 'same context provider. This is currently unsupported.', ); context._currentRenderer2 = rendererSigil; } } }
- 进入
calculateChangedBits
export function calculateChangedBits<T>( context: ReactContext<T>, newValue: T, oldValue: T, ) { // Use Object.is to compare the new context value to the old value. Inlined // Object.is polyfill. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is // 使用这种方法可更准确判断两个值是否相等, 符合下面条件,被认为是全等的 if ( (oldValue === newValue && (oldValue !== 0 || 1 / oldValue === 1 / (newValue: any))) || (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare ) { // No change 没有变化 return 0; } else { // 注意,这里 _calculateChangedBits 这个API未开放,直接忽略即可 const changedBits = typeof context._calculateChangedBits === 'function' ? context._calculateChangedBits(oldValue, newValue) : MAX_SIGNED_31_BIT_INT; if (__DEV__) { warning( (changedBits & MAX_SIGNED_31_BIT_INT) === changedBits, 'calculateChangedBits: Expected the return value to be a ' + '31-bit integer. Instead received: %s', changedBits, ); } return changedBits | 0; // | 0 这里是去除小数部分 } }
- 上述
(oldValue !== 0 || 1 / oldValue === 1 / (newValue: any)
表示 -0 !== +0 - 上述
oldValue !== oldValue && newValue !== newValue
表示 NAN !== NAN - 上述
MAX_SIGNED_31_BIT_INT
转换成2进制就是 32 位 的 1 - 以上判断就是 除了全等就是
MAX_SIGNED_31_BIT_INT
- 上述
- 进入
propagateContextChange
export function propagateContextChange( workInProgress: Fiber, context: ReactContext<mixed>, changedBits: number, renderExpirationTime: ExpirationTime, ): void { // 拿到 当前 provider 的第一个子节点 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; // Visit this fiber. let dependency = fiber.firstContextDependency; // 读取这个属性 // 存在,则进入循环 if (dependency !== null) { do { // Check if the context matches. // changedBits 是32位的二进制数都是1 // 只要 dependency.observedBits 不是0,dependency.observedBits & changedBits 就不是 0 // dependency.context === context 表示遍历过程中的组件是依赖于这个当前的context的 // 如果这个context的变化,那么说明它要重新渲染 // 同时它去判断它提供的这个 observedBits 跟 changedBits 它们是有相交的部分的 // 说明它依赖的部分也变化了, 通过这种方式判断这个组件其实是需要更新了 if ( dependency.context === context && (dependency.observedBits & changedBits) !== 0 ) { // Match! Schedule an update on this fiber. // 这个组件如果需要更新,除非是他自己调用了setState来创建了一个更新 // 不然的话没有外部的方式让他去可以更新这种情况 // 因为我们在 beginWork 的时候开始是要判断每一个组件自己的 expirationTime 的 // 如果那个组件它本身没有创建过更新,那么它的 expirationTime 是 nowork // 是nowork的话,它就直接跳过更新了,这明显不符合我们这边的一个context的一个需求 // context这边它就遍历到这个节点的时候,发现它依赖这个context,如何更新? // 通过主动去创建 update,并且设置你的 update.tag 是 ForceUpdate // 这其实没有state的一个更新,但是你必须要更新 // 因为依赖了这个context,context更新了,所以强制更新一下 // 然后把这个update执行 enqueueUpdate 一下,这样的话 // 在后续要更新到这个组件的时候,它就会发现它上面是有update的, 需要去更新它 if (fiber.tag === ClassComponent) { // Schedule a force update on the work-in-progress. const update = createUpdate(renderExpirationTime); update.tag = ForceUpdate; // TODO: Because we don't have a work-in-progress, this will add the // update to the current fiber, too, which means it will persist even if // this render is thrown away. Since it's a race condition, not sure it's // worth fixing. enqueueUpdate(fiber, update); } // 同时这边创建 update 是不够的,还要对这个 fiber 它的 expirationTime 进行一个操作 // 要看它,如果目前它的 expirationTime 的优先级是要大于我当前正在渲染这次 expirationTime 的 // 那么我就把它的 expirationTime 设置为这次 update,让它在这次渲染过程当中,肯定会被更新到 if (fiber.expirationTime < renderExpirationTime) { fiber.expirationTime = renderExpirationTime; } // 不仅要在这个 workingprogress 上面去设置,我还要对它的 alternate,也就是current也要进行一个设置 // 因为这个东西它们应该是要被同步的 let alternate = fiber.alternate; if ( alternate !== null && alternate.expirationTime < renderExpirationTime ) { alternate.expirationTime = renderExpirationTime; } // Update the child expiration time of all the ancestors, including // the alternates. // 因为在这个过程当中给这个组件创建了一个update // 代表的意思是,我父链上面的每一个节点,它的 childExpirationTime 有可能会被改变 // 同样的要执行一遍,类似于我们在 scheduleWorkToRoot 的时候的做的事情,就是设置它的 childExpirationTime let node = fiber.return; while (node !== null) { alternate = node.alternate; if (node.childExpirationTime < renderExpirationTime) { node.childExpirationTime = renderExpirationTime; if ( alternate !== null && alternate.childExpirationTime < renderExpirationTime ) { alternate.childExpirationTime = renderExpirationTime; } } else if ( alternate !== null && alternate.childExpirationTime < renderExpirationTime ) { alternate.childExpirationTime = renderExpirationTime; } else { // Neither alternate was updated, which means the rest of the // ancestor path already has sufficient priority. break; } node = node.return; } } // 最后设置 nextFiber = fiber.child; dependency = dependency.next; // 从这里可以看出 dependency 它也是可以有多个的 } while (dependency !== null); } else if (fiber.tag === ContextProvider) { // Don't scan deeper if this is a matching provider nextFiber = fiber.type === workInProgress.type ? null : fiber.child; } else { // Traverse down. nextFiber = fiber.child; } // 接下去和很多地方差不多的,就是它往子树上去找 // 如果子树上没有了,它就往它的兄弟节点去找 // 就是相当于要把我们的这个provider当前的这个组件,它的子树的每一个节点去遍历到 // 并且找到有 firstContextDependency 这个属性的这些节点 // 给它去创建更新的一个过程 if (nextFiber !== null) { // Set the return pointer of the child to the work-in-progress fiber. nextFiber.return = fiber; } else { // No child. Traverse to next sibling. nextFiber = fiber; while (nextFiber !== null) { if (nextFiber === workInProgress) { // We're back to the root of this subtree. Exit. nextFiber = null; break; } let sibling = nextFiber.sibling; if (sibling !== null) { // Set the return pointer of the sibling to the work-in-progress fiber. sibling.return = nextFiber.return; nextFiber = sibling; break; } // No more siblings. Traverse up. nextFiber = nextFiber.return; } } fiber = nextFiber; } }
- 关于
firstContextDependency
- 在 packages/react-reconciler/src/ReactFiberBeginWork.js#L1415 中的
updateContextConsumer
函数中- 调用了
prepareToReadContext
函数, 在这个函数中workInProgress.firstContextDependency = null;
- 调用了
- 接下去调用了
readContext
获取到 newValue- 在
lastContextDependency
为 null 的时候- 设置了
currentlyRenderingFiber.firstContextDependency = lastContextDependency = contextItem;
- 设置了
- 这是第一次进来的逻辑,对于 consumer 的情况已经够了,只会调这么一次,一个consumer 对应一个 provider 的,只会有一个context
- 这里面有很多变量控制和单项链表的数据结构来进行存储不同的 context
- 猜测是为了实现支持 hooks 的环境里面,一个 function component里面是可以读取多个context的使用的
- 就是 useContext 这个API, 读取多个context,说明这个function component是依赖于多个context provider
- 那么这个时候我们就要在这个 firstcontextdependency 上面去设置一个链表了
- 能够让我们在有多个context的情况下,每个context变化都能让这个function component去执行一次更新
- 后续的contextitem直接设置为当前的这个next
- 因为这个contextItem里面是有个next的指针的指向一个它依赖的context就可以了
- 在
- 拿到 newValue 之后,调用 render 拿到 newChildren
- 之后,reconcileChildren 就完成了,这就是 consumer 的一个更新过程
- 在 packages/react-reconciler/src/ReactFiberBeginWork.js#L1415 中的
- 注意,在之前的 packages/react-reconciler/src/ReactFiberClassComponent.js#L997 里面
const contextType = ctor.contextType;
声明了一个叫做 contextType 的这么一个属性- 这个属性拿到之后,就可以去
readContext
- 对于
updateClassComponent
packages/react-reconciler/src/ReactFiberBeginWork.js#L428- 也会调用 prepareToReadContext
- 所以对于 class component 这个fiber对象,也会给它设置 firstContextDependency 这个属性的
- 这就是在
propagateContextChange
这个方法里面可以看到- 它只有对于class component它才会去创建一个update
- 对于consumer它是不会去创建update的,因为consumer它不是update的一个载体
- 我们只需需要给它设置 expirationTime 就可以了, 不需要给它建update来告诉它,我需要 forceUpdate
- 因为在 consumer 中如果需要更新,就会执行更新的流程
- 它没有像 class component,可以通过 scu 这种方式的规避一个组件的更新的流程
- 关于
总结
- 以上就是新的context API它的一个实现过程
- 最主要的就是通过去更新 provider 的时候,给这个context它的
_currentValue
设置了一个值 - 设置的这个值就是新的context,它提供的这个value,以此让我们的consumer可以去获取到这个值
- 可以看到它
readcontext
的时候,最后是有一句代码的 return isPrimaryRenderer ? context._currentValue : context._currentValue2;
- 这个才是真正返回值的情况, 前面是处理它这个context的依赖
- 对于 provider 更新了之后,如何去通知一个consumer依赖的那个组件
- 通过手动的去遍历它的所有子节点, 然后去给它指定它的 expirationTime,告诉后续的更新的流程里面
- 这个依赖于我的context,所以它需要去更新
- 而且对于 class component,要单独为它去创建一个update
- 这就是 provider 和 consumer 这种新的 context API 它的一个实现过程