一、context是什么
引用react官网中的话:Context 是一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。很好理解。context就是react中共享状态的api。且具有跨越层级的能力。
何时使用context,引用官网的话是:Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
作者认为简单的共享状态需求完全可以使用context实现。如果遇到较复杂的需求可以考虑使用redux,mobx等社区方案。
二、如何使用context
react通过使用createContext生成一个context对象。其会暴露出提供者Provider。消费者Consumer。
- Provider接收一个
value属性,传递给消费组件。
三种消费context的方式
- Consumer可以让你在函数组件中订阅context
- useContext也可以让你在函数组件中使用context
- contextType可以让你在类组件中通过使用this.context来获取最近的context
列子
// context文件
import { createContext } from "react";
const rootContext = createContext(null)
export default rootContext
// 容器组件
function App () {
return <Provider value={{
name: 'huyunkun'
}}>
<Lib />
</Provider>
}
// 子组件
import React, { useContext } from 'react'
export default function Lib () {
const contextData = useContext(rootContext) || {}
const { name } = contextData
return <div>huyunkun</div>
}
对于context的使用作者在此不过多的阐述。有兴趣的通过可以阅读官网文档。该文默认读者熟练使用context。该文的侧重点为context的实现原理
三、context原理解析
我们从列子中可以看到context对象是通过createContext生成的。也就是说createContext是“梦”开始的地方。那么让我们来看看“梦“长什么样子。
function createContext (defaultValue) {
var context = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_threadCount: 0,
// These are circular
Provider: null,
Consumer: null,
// Add these to use same hidden class in VM as ServerContext
_defaultValue: null,
_globalName: null
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context
};
{
var Consumer = {
$$typeof: REACT_CONTEXT_TYPE,
_context: context
};
context.Consumer = Consumer;
}
{
context._currentRenderer = null;
context._currentRenderer2 = null;
}
return context;
}
我们可以看到createContext方法创建了一个context对象并放回出来。接下来看看context中的重要属性
- _currentValue:用来存储Provider传入的value(这里有两个_currentValue,主要是考虑到并发渲染)
- Provider:提供者
- Consumer:消费者
我们看到Provider和Consumer是两个React Element对象,且其中的_context都是指向了context
Provider属性
对于Provider的研究我们的主要关注点在于
- Provider如何传递context状态
- Provider的中value改变了,如果通知消费者更新组件
阅读上文可知,Provider本质上是一个react Element对象。所以我们使用jsx语法写的Provider会变为一个React Element对象。React Element对象最终变为一个fiber对象。该fiber的tag会被赋值为ContextProvider。在react的reconcile阶段会进入到beginWork中。并在beginWork中处理ContextProvider类型的fiber节点。
整体数据结构流转为 jsx -> React Element -> Fiber
那接下来让我们看看beginWork中是如何处理ContextProvider类型的fiber。
function beginWork (current, workInProgress, renderLanes) {
// ...省略
switch (workInProgress.tag) {
// ...省略
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
// ...省略
}
// ...省略
}
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; // Provider上新的value
// 将新的value 赋值给context
pushProvider(workInProgress, context, newValue);
{
if (oldProps !== null) {
var oldValue = oldProps.value;
if (objectIs(oldValue, newValue)) {
// value值未改变直接进入bailout逻辑
// No change. Bailout early if children are the same.
if (oldProps.children === newProps.children && !hasContextChanged()) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
// value如果改变了则开始查找需要更新的子孙路径
propagateContextChange(workInProgress, context, renderLanes);
}
}
}
var newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
我们看到ContextProvider类型的处理会进入到updateContextProvider函数中。
- 该函数中通过pushProvider将Provider中新的value赋值给context.
- 然后比较oldProps与newProps是否相等,和是否有context变化,判断是否进入bailout逻辑。
- 如果value有变化则会进入到propagateContextChange函数中。通过propagateContextChange执行propagateContextChange_eager函数。
function propagateContextChange_eager (workInProgress, context, renderLanes) {
while (fiber !== null) {
var nextFiber = void 0; // Visit this fiber.
// fiber 中存放context依赖项的列表
var list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
var dependency = list.firstContext;
// 向下遍历找到消费了该context的所有子孙节点
while (dependency !== null) {
// Check if the context matches.
if (dependency.context === context) {
// Match! Schedule an update on this fiber.
if (fiber.tag === ClassComponent) {
// Schedule a force update on the work-in-progress.
var lane = pickArbitraryLane(renderLanes);
var update = createUpdate(NoTimestamp, lane);
// 类组件添加上ForceUpdate 进行强制更新
update.tag = ForceUpdate; // TODO: Because we don't have a work-in-progress,
}
// ...省略
// 向上遍历修改路径上的childLanes 表明需要更新
scheduleContextWorkOnParentPath(fiber.return, renderLanes, workInProgress); // Mark the updated lanes on the list, too.
// ...省略
list.lanes = mergeLanes(list.lanes, renderLanes); // Since we already found a match, we can stop traversing the
// dependency list.
break;
}
dependency = dependency.next;
}
}
fiber = nextFiber;
}
}
由于该函数比较长,此处只贴出了核心部分。让我们一起来看一看:
该函数主要分为两部
- 从当前fiber开始向下递归遍历,找出所有的子fiber,并比较子fiber中
dependencies的context和当前Provider的context是否相同,如果相同且fiber类型为类组件类型,则会给类组件类型标记上ForceUpdate的标记。该标记表示强制刷新类组件。然后会提高fiber的优先级。让fiber能继续调和。 - 接下来会从该fiber开始向上找出父级fiber,并提高该链路上所有父级fiber的优先级。
如此一来,从根节点一直往下所有的需要调和的fiber节点都提高了优先级。在接下来的调和中便会按照此链路调和。看下图列子:
stateDiagram-v2
App --> com1
App --> com2
com1 --> App
com1 --> com3
com3 --> com1
com1 --> com4
假设有如上一棵fiber树,并且在com1上消费了app上的提供的Provider,则会按照上图顺序遍历fiber树。且app。com1,com3的优先级都会提高等待更新。
介绍完了Provider的作用和流程。我们会发现一个问题。
- 上文中说到的
dependencies是哪里来的?且dependencies是如何与context建立联系的。
我们在上文中说过。消费context可以使用Consumer,useContext,,contextType。因此我们可以联想到必然在这三个api中建立了与context的联系。我们用Consumer举列:
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
我们可以看到,类似Provider,在beginWork中Consumer是作为ContextConsumer类型处理。我们看到updateContextConsumer函数。
function updateContextConsumer (current, workInProgress, renderLanes) {
var context = workInProgress.type; // The logic below for Context differs depending on PROD or DEV mode. In
var newProps = workInProgress.pendingProps;
// 获取子fiber(该处为一个函数)
var render = newProps.children;
prepareToReadContext(workInProgress, renderLanes);
// 获取最新的context,并且与context建立关系
var newValue = readContext(context);
{
markComponentRenderStarted(workInProgress);
}
var newChildren;
{
newChildren = render(newValue);
}
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
代码删除了一部分,提取了关键逻辑。
我们可以看到updateContextConsumer的核心逻辑分为两部
- 通过
readContext获取到最新的value。(且在readContext与context建立关系) - 通过调用render函数,并传入最新的value,得到children。
我们先看到readContext函数:
function readContext (context) {
// 读取_currentValue 最新的value值
var value = context._currentValue;
// 如果没有dependencies 则构建一个contextItem加到dependencies链表中
if (lastFullyObservedContext === context); else {
var contextItem = {
context: context,
memoizedValue: value,
next: null
};
if (lastContextDependency === null) {
if (currentlyRenderingFiber === null) {
throw new Error('Context can only be read while React is rendering. ' + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + 'In function components, you can read it directly in the function body, but not ' + 'inside Hooks like useReducer() or useMemo().');
} // This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: contextItem
};
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}
该函数干了两件事:
- 先读取
_currentValue,也就是最新的value值。并将value值返回。 - 判断fiber上是否存在
dependencies,如果不存在,则构建一个contextItem,加入到dependencies链表中。如果存在则将构建的contextItem加入到链表的最后。
这样消费则便和提供者建立起了联系。
其实useContext,contextType也是通过readContext来建立联系的,原理相同。
useContext: function (context) {
currentHookNameInDev = 'useContext';
mountHookTypesDev();
return readContext(context);
}
if (typeof contextType === 'object' && contextType !== null) {
context = readContext(contextType);
}
多个 Provider 嵌套
如果存在多个Provider的情况,则会取离当前fiber最近的一个Provider的值。
四、总结流程
最后让我们一起来总结一下context的执行流程:
Consumer,useContext,contextType通过readContext与Provider建立联系。并在fiber上创建了dependencies。dependencies以链表的形式保存不同的Provider。- 当
Provider中的value改变时,会向下遍历所有的子fiber,找出与当前Provider相同的context。消费该context的父级fiber都会更新优先级。如果遇到类组件则给类组件打上ForceUpdate标记。
今天就到这里。欢迎大家评论区讨论。点赞。感谢各位读者