31. 原理篇-Context原理
接下来将介绍 context
原理。重点流程放在 context 的传递和更新两个方面。对于原理部分,我在这里只介绍了新版本 Context 的原理。感兴趣的同学可以看一下源码。
以 React 16.8
为例子🌰:
新版本 Context 位置:
react-reconciler/src/ReactFiberNewContext.js
老版本 Context 位置:
react-reconciler/src/ReactFiberContext.js
希望大家带着这些问题去阅读
1 Provder 如何传递 context?
2 三种获取 context 原理 (
Consumer
,useContext
,contextType
)?3 消费
context
的组件,context 改变,为什么会订阅更新 (如何实现) 。4 context 更新,如何避免
pureComponent
,shouldComponentUpdate
渲染控制策略的影响。5 如何实现的 context 嵌套传递 ( 多个 Povider )?
1 context 对象
上述所说的老版本 context 就是 Legacy Context 模式下的 context ,老版本的 context 是采用约定式的使用规则,于是有了 getChildContext
和 childContextTypes
协商的属性和方法,这种方式不仅不够灵活,而且对于函数组件也存在局限性,所以在 v16.3
推出了新版本的 context
,开发者能够更加灵活的运用 Context。新版本引入 context 对象的概念,而且 context 对象上除了保留了传递的信息 value
外 , 还有提供者 Provder
和消费者 Consumer
。
context 对象
要想吃透 context ,首先要研究一下 Context 对象是什么。上述讲到可以通过 createContext
创建一个 context 。那么万物之源就是这个 API ,接下来一起揭开 context 对象面纱。
export function createContext(defaultValue,calculateChangedBits){
/* context 对象本质 */
const context = {
$$typeof: REACT_CONTEXT_TYPE, /* 本质上就是 Consumer element 类型 */
_calculateChangedBits: calculateChangedBits,
_currentValue: defaultValue,
_threadCount: 0,
Provider: null,
Consumer: null,
};
/* 本质上就是 Provider element 类型。 */
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
context.Consumer = context
}
如上可以很容易的看清楚 context 对象的本质,这里重点介绍三个属性
Provider
本质上是一个 element 对象 $$typeof ->REACT_PROVIDER_TYPE
Consumer
本质上也是一个 element 对象 $$typeof ->REACT_CONTEXT_TYPE
_currentValue
这个用来保存传递给 Provider 的 value 。
Provider 提供者
上述明白了 Provider 本质上是一个特殊的 React Element 对象,那么接下来重点看一下 Provider 的实现原理,研究 Provider 重点围绕这两个点。
Provider 如何传递 context 状态的。
Provider 中 value 改变,如何通知订阅 context。
之前的章节讲述了 jsx -> element -> fiber 的流程,按照这个逻辑,接下来看一下 Provdier 的处理。
首先标签形式的
<Provider>
本质上就是REACT_PROVIDER_TYPE
的 React Element 。<Provider>
->REACT_PROVIDER_TYPE
React element 。接下来 element 会转化成 fiber ,fiber 类型为 ContextProvider , React element ->
ContextProvide fiber
。
ContextProvider 类型的 fiber ,在 fiber 调和阶段会进入到 beginWork
流程,这个阶段会发生两件事。
如果当前类型的 fiber 不需要更新,那么会
FinishedWork
中止当前节点和子节点的更新。如果当前类型 fiber 需要更新,那么会调用不同类型 fiber 的处理方法。当然
ContextProvider
也有特有的 fiber 更新方法 ——updateContextProvider
,那么如果想要深入Provder
的奥秘,有必要看一下这个方法做了些什么?
react-reconciler/src/ReactFiberBeginWork.js
function updateContextProvider(current ,workInProgress,renderExpirationTime,) {
/* 获取 Provder 上的 value */
pushProvider(workInProgress, newProps.value;);
/* 更新 context */
if (oldProps !== null) {
const changedBits = calculateChangedBits(context, newProps.value;, oldProps.value);
if (changedBits === 0) {
//context 没有变化。如果孩子们都是一样的话。那么不需要更新
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return ... // 停止调合子节点,收尾工作
}
} else { /* context 改变,更新 context */
propagateContextChange( workInProgress,context, changedBits, renderExpirationTime,);
}
}
/* 继续向下调和子代 fiber */
...
}
如上保留了 updateContextProvider
的核心流程如下:
第一步: 首先会调用
pushProvider
,pushProvider
会获取 type 属性上的 _context 对象,就是上述通过createContext
创建的 context 对象。然后将 Provider 的 value 属性,赋值给 context 的 _currentValue 属性上。这里解释了 Provder 通过什么手段传递 context value,即通过挂载 context 的 _currentValue 属性。第二步: 通过
calculateChangedBits
计算出 changedBits ,calculateChangedBits
内部触发 context 对象上的_calculateChangedBits
,细心的同学可以发现,在调用createContext
的时候,实际上是有第二个参数的calculateChangedBits
,在更新 Provider 的时候这个参数就派上用场了,当它返回的changedBits === 0
的时候,那么还会浅比较 children 是否发生变化,还有就是有没有legacy context
,如果这三点都满足的话,那么会判断当前 Provider 和子节点不需要更新,那么会 return 停止向下调和子节点。第三步(重点):在实际开发中,绝大多数当 value 发生变化,会走
propagateContextChange
这个流程,也是 Provider 更新的特点。那么这个方法到底做了些什么呢?接下来重点看一下这个函数做了些什么?
propagateContextChange 函数流程很繁琐,这里简化了流程,保留了最核心的部分。
function propagateContextChange(workInProgress,context){
let fiber = workInProgress.child;
if (fiber !== null) {
fiber.return = workInProgress;
}
while(fiber !== null){
const list = fiber.dependencies;
while (dependency !== null) {
if (dependency.context === context){
/* 类组件:不受 PureComponent 和 shouldComponentUpdate 影响 */
if (fiber.tag === ClassComponent) {
/* 会走 forceUpdate 逻辑 */
const update = createUpdate(renderExpirationTime, null);
update.tag = ForceUpdate;
enqueueUpdate(fiber, update);
}
/* 重要:TODO: 提高 fiber 的优先级,让当前 fiber 可以 beginWork ,并且向上更新父级 fiber 链上的优先级 */
...
}
}
}
}
propagateContextChange
非常重要,它的职责就是深度遍历所有的子代 fiber ,然后找到里面具有 dependencies
的属性,对比 dependencies 中的 context 和当前 Provider 的 context 是否是同一个,如果是同一个,那么如果当前 fiber 是类组件,那么会给绑定一个 forceUpdate 标识 。然后会提高 fiber 的更新优先级,让 fiber 在接下来的调和过程中,处于一个高优先级待更新的状态。接下来的代码比较长,我这里没有全部罗列出来,大致逻辑就是,找到当前 fiber 向上的父级链上的 fiber ,统一更新他们的优先级,使之变成高优先级待更新状态。
那么上述流程中暴露出几个问题:
1 什么情况下 fiber 会存在 dependencies ,首先 dependencies 在第十七章中会讲到,它保存的是 context 的依赖项,那么什么情况下会存在 context 依赖项。
2 为什么对于 class 类组件会创建一个 ForceUpdate 类型的 update 对象呢? 知其然,知其所以然,首先看一下它是什么?
|--------问与答--------|
问: ForceUpdate 类型 update: 什么是 forceUpdate 类型的 update 呢?
答:在类组件中,通过调用 this.forceUpdate()
带来的更新就会被打上 ForceUpdate 类型的 update tag,这里可以理解为强制更新。 生命周期章节讲过, 在类组件更新流程中,强制更新会跳过 PureComponent
和 shouldComponentUpdate
等优化策略。|---------end---------|
3 存在 dependency 的 fiber ,为什么要向上更新父级 fiber 链上的优先级,让所有父级 fiber 都处于一个高优先级。
对于上面这三个问题,跟上我的思路逐一突破。
第一个问题: 首先就是 dependencies 属性,这个属性可以把当前的 fiber 和 context 建立起关联,那么可以理解成,使用了当前 context 的 fiber 会把 context 放在 dependencies 中,dependencies 属性本身是一个链表结构,一个 fiber 可以有多个 context 与之对应。反过来推测一下,什么情况下会使用 context 呢。那么有以下几种可能:
1 有 contextType
静态属性指向的类组件。
2 使用 useContext
hooks 的函数组件。
3 context 提供的 Consumer
。
那么可以大胆的推测一下,使用过 contextType useContext 的组件对应 fiber,和 Consumer 类型 fiber,会和 dependencies 建立起联系,会把当前消费的 context 放入 dependencies 中。这个下面会给详细解释
第二个问题: 为什么对于 class 组件会创建一个 ForceUpdate 的 update 呢?
在生命周期章节和渲染控制章节,讲到过如果想要让类组件调用 render,得到新的 children,那么就要通过 PureComponent
和 shouldComponentUpdate
等层层阻碍,那么 context 要突破这些控制,就要做到当 value 改变,消费 context 的类组件更新,则需要通过 forceUpdate 强制更新。这样就解决了类组件更新限制。
那么总结一下流程,当 Provider 的 value 更新之后,Provider 下面的只要有消费了 context 的类组件,就会触发强制更新。这也就解释了最开始的问题——context 更新,如何避免 pureComponent
, shouldComponentUpdate
渲染控制策略的影响。 用一幅流程图表示:
第三个问题: 这个问题就要从 Provider 类型的 fiber 调和开始讲。
Provider 和 beiginWork 调和更新机制
接下来重点介绍 Provider 和 beiginWork 调和更新机制。首先引出两个思考点:
第一个类组件执行 render ,函数组件执行就是渲染么?
第二个 Context 改变如何做到消费 context 的组件更新的?(更新原理)
先来看一下第一个思考点,关于渲染的思考,实际上在 React 整个 Reconciler
调和流程中,从更新调度任务的发起,再到在 commit 和 render 两大阶段,再到真实的 dom 元素绘制,每一个环节都属于渲染的一部分。而开发者能够控制的 render ,只是其中的一小部分——类组件执行 render ,函数组件执行。而且这些本质上都发生在 FunctionComponent fiber 和 ClassComponent fiber 上。但是整个 fiber 树在调和阶段都需要更新的。更新调和 fiber 的方法在 React 底层叫做 beginWork
。有一个问题需要注意,就是 beginWork
非 render。先来看看两者的区别。
beginWork
: 在一次更新中,只要需要更新的 fiber 或者受到牵连的 fiber,都会执行 beginWork 。render
: 在一次更新中,只有组件类型的 fiber 会执行 render ,得到新的 children 。如果组件触发 render 那么它一定经历过beginWork
这里如果有同学不明白不要紧,接着往下看。
比如发生一次更新任务,此次更新可能发生整个 fiber 树的任意枝叶上,但是因为 context props 穿透影响,React 不知道此次更新的波及范围,那么如何处理呢? React 会从 rootFiber 开始更新,每一个更新 fiber 都会走 beginWork
流程,开始找不同,找到有没有需要更新的地方,那么指标是什么呢,其中一个重要的指标就是更新的优先级,老版本用的是 expirationTime
,新版本用的是 lane
,那么就要保证一个问题,就是如果更新发生在一个子代节点,那么只有父节点 beginWork
才能让子代节点 beginWork
。这样就形成了一条 root fiber -> 父 fiber -> 子 fiber 的 beginWork
链。在 beginwork 过程中,有几种情况:
第一种: 如果遇到组件,而且更新不涉及当前组件,也不在当前组件的父子递归链上,那么就不会 render,也不会向下 beginWork 。
第二种: 如果遇到组件,而且更新不涉及当前组件,但是更新组件属于当前组件的子孙后代,那么不会 render,但是会向下 beginWork ,目的很明确,找到对应的更新组件。
第三种: 如果遇到其他类型的 fiber 比如 hostComponent
<div>
,那么会对比当前的更新优先级,如果低优先级,那么不需要向下 beginWork 。反之向下 beginWork。
这么说可能大家不是很理解,我举一个例子:
如下当点击 componentB 下面的 span 触发 setState 更新 ,如下可以清晰看见 beginWork 和 render 流程。
从 root 开始第一次调和, 三个 fiber 都会经历 beginWork ,通过对比优先级,
componentA
和div
停止向下 beginwork。更新发生在 componentB ,所以 componentB 渲染,触发
render
,得到新的 element,通过对比,div
span
都会 beginwork。componentC 由于父组件更新,没有任何优化策略的情况,那么也会跟着
render
,接着 div 也会跟着 beginwork。
那么如上,如果 componentC 通过 PureComponent
或者 shouldComponentUpdate
限制更新之后。那么会变成如下的样子:
如上 componentC 通过
PureComponent
处理后,不再 render ,当然也不会再向下 beginwork。
接下来,如果点击 componentC 下的 div,触发 setState 更新,那么又会发生什么呢?
此时更新发生在
componentC
上,所以 componentB 只会发生 beginwork ,不会发生 render。componentB
下面的div
会停止向下的 beiginwork 。
我们总结流程如下:
1 如果一个组件发生更新,那么当前组件到 fiber root 上的父级链上的所有 fiber ,更新优先级都会升高,都会触发 beginwork 。
2 render 不等于 beginWork,但是 render 发生,一定触发了 beginwork 。
3 一次 beginwork ,一个 fiber 下的同级兄弟 fiber 会发生对比,找到任务优先级高的 fiber 。向下 beginwork 。
对于 beginwork 的流程,接下来会有专门的章节维护。
Context 原理
接下来言归正传,我们来研究一下 context 的更新原理,上面说到 Provider
更新,会递归所有的子组件,只要消费了 context 的子代 fiber ,都会给一个高优先级。而且向上更新父级 fiber 链上的优先级,让所有父级 fiber 都处于一个高优先级。那么接下来高优先级的 fiber 都会 beginWork 。
那么将上述例子进行修改,propagateContextChange
的流程会下如下一样,把父级 fiber 的优先级提高。
那么整个 fiber 更新流程会像如下一样
2 Consumer
我们已经讲了 Provider 核心原理,还有另外一部分就是 Consumer ,研究一下其原理。
Consumer 流程
上文说道,Consumer 本质上是类型为 REACT_CONTEXT_TYPE
的 element 对象。在调和阶段,会转化成 ContextConsumer
类型的 fiber 对象。在 beginwork 中,会调用 updateContextConsumer
方法。那么这个方法做了些什么呢?
react/react-reconcider/src/ReactFiberBeginWork.js
function updateContextConsumer(current,workInProgress,renderExpirationTime,) {
let context = workInProgress.type;
const newProps = workInProgress.pendingProps;
/* 得到 render props children */
const render = newProps.children;
/* 读取 context */
prepareToReadContext(workInProgress, renderExpirationTime);
/* 得到最新的新的 context value */
const newValue = readContext(context, newProps.unstable_observedBits);
let newChildren;
/* 得到最新的 children element */
newChildren = render(newValue);
workInProgress.effectTag |= PerformedWork;
/* 调和 children */
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
return workInProgress.child;
}
updateContextConsumer
的核心流程:
首先调用
readContext
获取最新的 value 。然后通过
render props
函数,传入最新的 value,得到最新的children
。接下来调和 children 。
那么有一个问题就是 fiber 上的 dependencies 如何和 context 建立起关联。 那么就是 readContext
这个函数做的事,可以提前透露一下,useContext 和 contextType 本质上也是
readContext
readContext 是除了 Provider
之外,第二个核心知识点。
react/react-reconcider/src/ReactFiberNewContext.js
export function readContext( context,observedBits ){
/* 创建一个 contextItem */
const contextItem = {
context,
observedBits: resolvedObservedBits,
next: null,
};
/* 不存在最后一个 context Dependency */
if (lastContextDependency === null) {
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
expirationTime: NoWork,
firstContext: contextItem,
responders: null,
};
} else {
/* 存在的情况 */
lastContextDependency = lastContextDependency.next = contextItem;
}
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
readContext 主要做的事情是这样的,首先会创建一个 contextItem ,上述说到过 fiber 上会存在多个
dependencies
,它们以链表的形式联系到一起,如果不存在最后一个context dependency
,那证明 context dependencies 为空 ,那么会创建第一个 dependency ,如果存在最后一个 dependency ,那么 contextItem 会以链表形式保存,并变成最后一个 lastContextDependency 。
多个 Provider 嵌套
如果有多个 Provider 的情况,那么后一个 contextValue 会覆盖前一个 contextValue,在开发者脑海中,要有一个定律就是:Provider
是用来传递 value,而非保存 value 。
3 contextType 和 useContext
useContext 原理
useContext
原理,调用 useContext 本质上调用 readContext
方法。
react/react-reconcider/src/ReactFiberHooks.js
const HooksDispatcherOnMount ={
useContext: readContext
}
函数组件通过 readContext ,将函数组件的
dependencies
和当前 context 建立起关联,context 改变,将当前函数组件设置高优先级,促使其渲染。
contextType 原理
类组件的静态属性 contextType
和 useContext 一样,本质上就是调用 readContext 方法。
react/react-reconcider/src/ReactFiberClassComponent.js
function constructClassInstance(workInProgress,ctor,props){
if (typeof contextType === 'object' && contextType !== null) {
/* 读取 context */
context = readContext(contextType);
}
const instance = new ctor(props, context);
}
静态属性 contextType ,在类组件实例化的时候被使用,本质上也是调用
readContext
将 context 和 fiber 上的dependencies
建立起关联。
4 Context 流程总结
下面对整个 context 原理部分做总结。
Provider 传递流程:Provider 的更新,会深度遍历子代 fiber,消费 context 的 fiber 和父级链都会提升更新优先级。 对于类组件的 fiber ,会 forceUpdate 处理。接下来所有消费的 fiber,都会 beginWork 。
context 订阅流程:
contextType
,useContext
,Consumer
会内部调用readContext
,readContext 会把 fiber 上的dependencies
属性和 context 对象建立起关联。
5 总结
本章节知识点总结:
context 原理,Provider 做了些什么。
beginWork 和 render 的更新原则和区别。
三种 context 传递模式原理。
context 订阅消费原理。
Provider 嵌套传递原理。
透漏一下,接下来会更新另外一个新的章节 fiber 初始化和调和流程。
最后更新于