作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
这个章节是 React 源码系列的最后一个章节了,主要来讲我们用于跨多层级通信的常用 hooks —— useContext,我们会从 context 的创建和消费讲起,再讲到怎么样使用 useContext 这个钩子来简化操作,最后分析 context 的运行原理和挂载过程。
Context 是 React 官方提供的一种让父组件可以为它下面的整个组件树提供数据的数据传递方式,它产生的原因是:
props 是将数据通过 UI 树显式传递到使用它的组件的好方法。但是当你需要在组件树中深层传递参数以及需要在组件间复用相同的参数时,传递 props 就会变得很麻烦。最近的根节点父组件可能离需要数据的组件很远,而状态提升到太高的层级会导致 “逐层传递 props” 的情况。
Context 多层级之间则是一种不需要 props 将数据“直达”到所需的组件中的数据传递方式
在 React 中,Context 的使用步骤如下:
下面是一个具体的例子:
首先我们使用 createContext
这个 API 来创建一个 Context ,它传入一个初始值,返回一个 context
const Context = React.createContext('default-value')
我们可以通过 Provider 包裹组件来提供这个 context,其中的 value 就是给子组件的这个 Provider 的初始值,下面的相当于 Context.Provider 包裹的所有子组件都可以通过 Context 来获取相同的数据,这些数据的初始值是 new-value。
注意:Provider 中提供的值才是 context 的默认值,createContext 初始化的值并不是默认值,只有当 Provider 未提供默认值时才会使用定义时的默认值。
const Context = React.createContext('default-value')
function Parent() {
return (
// 在内部的后代组件都能够通过相同的 Ract.createContext() 的实例访问到 context 数据
<Context.Provider value="new-value">
<Children>
<Context.Provider>
)
}
而我们的子组件可以通过的来消费我们的 context ,这里我们需要从之前定义 Context 的位置将其引入,只有使用了同一个 Context 才能获取相同的数据
import Context from "xxxxxx"
<Context.Consumer>
{ v => {
// 内部通过函数访问祖先组件提供的 Context 的值
return <div> {v} </div>
}}
</Context.Consumer>
我们可以通过修改 Provider 提供的值来改变 Context 的内容,以下是一个例子:
class App extends Component {
setLanguage = language => {
this.setState({ language });
};
state = {
language: "en",
setLanguage: this.setLanguage
};
render() {
return (
<LanguageContext.Provider value={this.state}>
<h2>Current Language: {this.state.language}</h2>
<p>Click button to change to jp</p>
<div>
<LanguageSwitcher />
</div>
</LanguageContext.Provider>
);
}
}
class LanguageSwitcher extends Component {
render() {
return (
<LanguageContext.Consumer>
{({ language, setLanguage }) => (
<button onClick={() => setLanguage("jp")}>
Switch Language (Current: {language})
</button>
)}
</LanguageContext.Consumer>
);
}
}
useContext
是一个 React Hook,可以让你读取和订阅组件中的 Context
const value = useContext(SomeContext)
它其实就是简化了我们消费 Context 的过程,我们不必再通过 Consumer 来获取需要的数据,而只要通过 useContext 就可以拿到 Context 内部的值:
import Context from "xxxxxx"
function Child() {
const { ctx } = useContext(Context)
return <div> {ctx} </div>
}
现在到了我们的重头戏,关于 context 的原理,我们先从 context 这个类的定义说起,它在我们的 packages/shared/ReactTypes.js 这个文件中:
export type ReactContext<T> = {
$$typeof: Symbol | number,
Consumer: ReactContext<T>, // 消费 context 的组件
Provider: ReactProviderType<T>, // 提供 context 的组件
// 保存 2 个 value 用于支持多个渲染器并发渲染
_currentValue: T,
_currentValue2: T,
_threadCount: number, // 用来追踪 context 的并发渲染器数量
// DEV only
_currentRenderer?: Object | null,
_currentRenderer2?: Object | null,
displayName?: string, // 别名
_defaultValue: T,
_globalName: string,
...
};
createContext 就是新建了这样一个数据结构,包括了数据、Consumer 和 Provider 来提供用户使用,它的代码在 packages/react/src/ReactContext.js 这个文件中:
export function createContext<T>(defaultValue: T): ReactContext<T> {
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE, // 用 $$typeof 来标识这是一个 context
_currentValue: defaultValue, // 给予初始值
_currentValue2: defaultValue, // 给予初始值
_threadCount: 0,
Provider: (null: any),
Consumer: (null: any),
_defaultValue: (null: any),
_globalName: (null: any),
};
// 添加 Provider ,并且 Provider 中的_context指向的是 context 对象
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE, // 用 $$typeof 来标识这是一个 Provider 的 symbol
_context: context,
};
let hasWarnedAboutUsingNestedContextConsumers = false;
let hasWarnedAboutUsingConsumerProvider = false;
let hasWarnedAboutDisplayNameOnConsumer = false;
// 添加 Consumer
context.Consumer = context;
return context;
}
在新建了我们的 Context 后,接下来就是 Context 的提供,我们知道 Context 使用 Provider 来提供 Context 内容,而这个
则是作为了一个 DOM 元素节点编写在我们的 jsx 代码中,那它的值是怎么样被提供给子元素的呢,我们来看:
上文中我们提到了,我们使用 $$typeof
来标识一个 Provider ,读过之前教程的读者应该对这个不陌生,我们在第一篇教程中提到了,我们的 ReactElement 中就是用这个字段来标识这是一个 react.element。同样,这里我们也用这个字段来标识 Provider 元素,这样我们在生成 Fiber 的时候就可以进行统一的处理。
代码在我们的 /packages/react-reconciler/src/ReactFiber.old.js 函数中,我们调用了 createFiberFromTypeAndProps 建立 Fiber,其中,我们通过传入的 type来判定,其中根据 $$typeof 的值给我们的 Fiber 的 tag 添加了不同的值,上文中,在创建 context 时,Provider 给予了 REACT_PROVIDER_TYPE 类型,而 Consumer 指向 context 本身,所以就是 REACT_CONTEXT_TYPE 类型字段,因而,当我们在 jsx 中解析到这两个类型时,就会判定为对应的字段:
export function createFiberFromTypeAndProps(
type: any, // React$ElementType,element的类型
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let fiberTag = IndeterminateComponent;
let resolvedType = type;
if (typeof type === 'function') {
// ....
} else if (typeof type === 'string') {
// ....
} else {
getTag: switch (type) {
// ....
default: {
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
case REACT_PROVIDER_TYPE:
fiberTag = ContextProvider;
break getTag;
case REACT_CONTEXT_TYPE:
fiberTag = ContextConsumer;
break getTag;
//.....
}
}
}
}
}
在判定了对应的类型后,我们继续看对 Fiber 的处理:我们在 beginWork 这个函数中,我们会对不同 tag 的进行处理,我们先来看ContextProvider 的处理:
function updateContextProvider(current, workInProgress, renderLanes) {
const providerType = workInProgress.type
const context = providerType._context
const newProps = workInProgress.pendingProps
const oldProps = workInProgress.memoizedProps
const newValue = newProps.value
pushProvider(workInProgress, context, newValue)
// 是更新
if (oldProps !== null) {
const oldValue = oldProps.value
// 可以复用
if (is(oldValue, newValue)) {
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
)
}
} else {
// 查找 consumer 消费组件,标记更新
propagateContextChange(workInProgress, context, renderLanes)
}
}
// 继续遍历
const newChildren = newProps.children
reconcileChildren(current, workInProgress, newChildren, renderLanes)
return workInProgress.child
}
function pushProvider(providerFiber, context, nextValue) {
// 压栈
push(valueCursor, context._currentValue, providerFiber)
// 修改 context 的值
context._currentValue = nextValue
}
function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
index++;
valueStack[index] = cursor.current;
cursor.current = value;
}
我们发现如果我们需要更新我们的 context ,它会调用 propagateContextChange 这个方法来标记更新,那么它的具体逻辑是什么呢?它主要调用了 propagateContextChange_eager 这个函数,我们来看一下这个函数:
深度优先遍历所有的子代 fiber ,然后找到里面具有 dependencies
的属性,,这个属性中挂载了一个元素依赖的所有 context,它的挂载会在下一节中提到,对比 dependencies
中的 context 和当前 Provider 的 context 是否是同一个;如果是同一个,它会创建一个更新,设定高 fiber 的更新优先级,类似于调用 this.forceUpdate 带来的更新:
function propagateContextChange_eager<T>(
workInProgress: Fiber,
context: ReactContext<T>,
renderLanes: Lanes,
): void {
let fiber = workInProgress.child;
if (fiber !== null) {
fiber.return = workInProgress;
}
// 深度优先遍历整个 fiber 树
while (fiber !== null) {
let nextFiber;
const list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
let dependency = list.firstContext;
// 获取 dependencies
while (dependency !== null) {
// 如果是同一个 context
if (dependency.context === context) {
if (fiber.tag === ClassComponent) {
const lane = pickArbitraryLane(renderLanes);
const update = createUpdate(NoTimestamp, lane);
// 高优先级,强制更新
update.tag = ForceUpdate;
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
} else {
const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
const pending = sharedQueue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress,
);
list.lanes = mergeLanes(list.lanes, renderLanes);
break;
}
dependency = dependency.next;
}
} else if (fiber.tag === ContextProvider) {
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
} else if (fiber.tag === DehydratedFragment) {
//... 省略
} else {
nextFiber = fiber.child;
}
// 深度优先遍历找到下一个节点
if (nextFiber !== null) {
nextFiber.return = fiber;
} else {
nextFiber = fiber;
while (nextFiber !== null) {
if (nextFiber === workInProgress) {
nextFiber = null;
break;
}
const sibling = nextFiber.sibling;
if (sibling !== null) {
sibling.return = nextFiber.return;
nextFiber = sibling;
break;
}
nextFiber = nextFiber.return;
}
}
fiber = nextFiber;
}
}
上文我们提到了对
节点的处理,那么之后我们来讲讲不同的消费方式的源码处理方式:
首先是 Context.Consumer 这种最常用的方式,它的处理还是在 beginWork
函数中,我们在上一部分讲到了, Consumer 指向 context 本身,所以就是 REACT_CONTEXT_TYPE 类型字段,其生成 fiber 时会识别 REACT_CONTEXT_TYPE 类型然后添加 ContextConsumer tag ,当我们识别到这个 tag ,就会调用 updateContextConsumer 进行处理。
updateContextConsumer 中的逻辑是先通过 prepareToReadContext 和 readContext 获取最新的 context 的值,再把最新的值传入子组件进行更新操作:
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes)
}
}
function updateContextConsumer(current, workInProgress, renderLanes) {
let context = workInProgress.type
context = context._context
const newProps = workInProgress.pendingProps
const render = newProps.children
// 准备读取 context
prepareToReadContext(workInProgress, renderLanes)
// 获取最新的 context
const newValue = readContext(context)
// 更新包裹的子组件
let newChildren
newChildren = render(newValue)
reconcileChildren(current, workInProgress, newChildren, renderLanes)
return workInProgress.child
}
prepareToReadContext 中把 currentlyRenderingFiber 设置为当前的节点,方便后续取用,如果当前节点没有 dependencies 链表,则初始化一个链表,这个链表用于我们挂载 context 元素。
而在 readContext 中,它收集组件依赖的所有不同的 context,则将 context 添加到 fiber.dependencies
链表中,之后返回我们的 context._currentValue 作为我们需要的值,这个生成的 dependencies 后续会在我们更新一个 context 时用到,我们在上面已经提到了
function prepareToReadContext(workInProgress, renderLanes) {
currentlyRenderingFiber = workInProgress
lastContextDependency = null
lastFullyObservedContext = null // 重置
const dependencies = workInProgress.dependencies
if (dependencies !== null) {
const firstContext = dependencies.firstContext
if (firstContext !== null) {
if (includesSomeLane(dependencies.lanes, renderLanes)) {
markWorkInProgressReceivedUpdate()
}
dependencies.firstContext = null
}
}
}
export function readContext<T>(context: ReactContext<T>): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// 不是可以使用 context 的时机
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
} else {
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}
useContext 作为一个用于 function 组件的钩子,它的作用方式和上文的直接消费基本一致,只是在作用的位置变成了在 hooks 的相关函数中。我们可以看到 useContext
的 OnMount
和 OnUpdate
其实就是调用了 readContext 函数,也就是我们上文的函数:
const HooksDispatcherOnMount: Dispatcher =
useContext: readContext,
//....
};
const HooksDispatcherOnUpdate: Dispatcher = {
useContext: readContext,
//....
};
最后我们把目光看到 commit 阶段,在这个阶段的 completeWork
函数中,我们调用了一个函数 popProvider
,这个函数和我们的之前的 pushProvider
相互呼应,抛出了栈中的一个元素:
function popProvider(providerFiber) {
var currentValue = valueCursor.current;
pop(valueCursor, providerFiber);
var context = providerFiber.type._context;
{
context._currentValue = currentValue;
}
}
function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
if (index < 0) {
return;
}
cursor.current = valueStack[index];
valueStack[index] = null;
index--;
}
现在我们来分析一下为什么要用这个栈来存储我们的 context:
假设我们有一段下文这样的代码,因为我们的 context 是跨层级的,如果我们在深度优先遍历的时候需要在组件中传递 context 的值,就需要就需要一层一层往下传递,这样就会出现额外的开销是不能接收的,因此我们用了一个全局的变量来记录它。但是 Provider 是可能会嵌套的,代码中也会有多个值不同的 Provider。
索性我们用的是深度优先搜索来遍历 Fiber 的,它是一个先进后出的递过程,所以我们可以用一个 stack 记录我们所有的 context ,当我们读取到一个 Provider 的时候,把数值放入栈中,这样它的孩子都能在运行过程中读取到这个值,如果其中嵌套了另一个 Provider,我们在 stack 中添加一位并且更新值,这样这个嵌套的 Provider 的孩子节点获取到的就是距离它最近的新的值,当这个 Provider 销毁的时候,我们从栈中抛出这个值,那么被上一级 Provider 包裹的其他子组件就会获取到 上一级 Provider 的值。
render() {
return (
<>
<TestContext.Provider value={10}>
<Test1 />
<TestContext.ProviderProvider value={100}>
<Test2 />
</TestContext.Provider>
</TestContext.Provider>
</>
)
}
上述的原理也说明了一点:为什么当 Context.Provider 的 value 值发生变化时,所有使用 Context 的组件会强制更新,因为其中可能涉及到嵌套 Provider 的情况,如果我们局部更新的话,就不能正常的更新我们的栈,我们需要销毁整个栈然后重新生成,才能保证其中的调用顺序不发生变化,因此我们需要重新渲染其包裹的所有子元素。
以上就是 context 相关的内容了,我们来总结一下:
createContext
创建了可以 context ,它初始化了数据、Consumer 和 Provider ,让他们指向同一个 ReactContext 对象来保证用户总是拿到了最新的 context ,ReactContext 的 _currentValue
属性上放着这个 context 的数据dependencies
链表中,因为 Consumer 指向ReactContext 本身,所以我们直接通过 _currentValue 就可以拿到需要的对象propagateContextChange
递归遍历所有的孩子节点,节点中使用了这个 Provider 的会被标识为强制更新优先级,在之后过程中被更新至此,我们一小节一小节的 React 源码教程已经更新完毕了,内容比较杂也花费了不少的时间来阅读和理解,之后会抽空写一篇总结和梳理性的文章作为这个教程的结束,也感谢读到这里的大家。