我应该如何去理解React Context
前言
React Context是React提供给开发者的一种常用的状态管理机制,利用Context可以有效的将同一个状态在多级组件中进行传递,并能够在状态更新时,自动的通知各个组件进行更新。那React Context又是如何做到这一点的,以及为什么需要这么设计呢?
为什么需要Context
在React的数据管理理念中,一直遵循着单项数据流以及数据不变性的理念。当我们需要从父组件将状态向子组件传递时,我们往往需要通过Props显式进行传递,例如:
const Father:FC = () => {
const [count, setCount] = useState<number>(0)
return (
<Son count={count} />
)
}
const Son:FC = (props) => {
const { count } = props;
return (
<span>{count}</span>
)
}
但是,倘若父组件需要向子组件的子组件,也就是孙组件进行状态的传递呢?或者父组件需要同时向多个子组件进行传递呢?当然,继续使用props进行逐层往下的显示传递肯定也是能实现这个需求的,但那样的代码未免过于繁琐且难以维护,如果能够在父组件里维护一个类似于Js里的全局变量,所有的子组件都能使用这个全局变量不就好了吗?是的,这个就是Context的作用,但又远远不止这么简单。
Context是什么?
Context提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
Context如何使用?
创建Context
首先,我们需要在父组件中, 利用React.createContext创建一个React Context对象,这个方法接受一个入参,作为当前Context的默认值。
import React from 'react'
const Context = React.createContext(defaultValue)
向下传递数据
利用Context对象返回的Provide组件,包裹需要传递数据的子组件。每个 Context 对象都会返回一个 Provider React 组件,它接收一个 value 属性,可将数据向下传递给消费组件。当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。
const Father:FC = () => {
const [count, setCount] = useState<number>(0)
return (
<Context.Provider value={count}>
<Son />
</Context.Provider>
)
}
接收数据
被包裹的子组件,利用useContext获取父组件传递的数据。
const Son:FC = (props) => {
const value = React.useContext(Context);
return (
<span>{value}</span>
)
}
Context如何以及为何这样实现?
让我们回到Context使用过程的第一步,通过阅读源码去研究createContext究竟做了什么样的工作?剔除了一些干扰代码,其实createContext做的事情其实非常简单,创建了一个对象,保存了当前context的value, 以及返回了一个Provide组件。
import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import type {ReactProviderType} from 'shared/ReactTypes';
import type {ReactContext} from 'shared/ReactTypes';
export function createContext<T>(defaultValue: T): ReactContext<T> {
// TODO: Second argument used to be an optional `calculateChangedBits`
// function. Warn to reserve for future use?
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
_currentValue: defaultValue,
_currentValue2: defaultValue,
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_threadCount: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
// Add these to use same hidden class in VM as ServerContext
_defaultValue: (null: any),
_globalName: (null: any),
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
return context;
}
在React编译的过程中,会将我们写的JSX语法代码,转化成React.createElement方法,执行这个方法后,会得到一个ReactElement元素对象,也就是我们所说的Virtual Dom。这个元素对象,会记录着当前组件所接收的入参以及元素类型。而Provide组件实际上编译完之后也是一个ReactElement,只不过他的Type跟正常的组件并不一样,而是context.Provider。
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
那么,子组件又是如何利用Provider和useContext去获取到最新的数据的呢?useContext接收一个context对象作为参数,从context._currentValue中读取当前contetx的value值。
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
// 获取当前context保存的value
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
// ...do something
// 返回当前的值
return value;
}
问题又来了,当父组件的状态改变时,又是如何通过Provider触发更新,通知订阅当前状态的子组件进行重新渲染的呢?当父组件的状态进行更新时,React整体会进入到调度更新阶段,Fiber节点会进入到beginWork的方法当中,在这个方法里面,会根据当前更新节点的类型,从而执行相对应的方法。上文提到,Provider组件是有单独的自己的类型ContextProvider的,所以会进入到相对应的更新方法,updateContextProvide。其实updateContextProvide里做的事情,大抵可以概括为:首先更新context._currentValue, 然后比较新老value是否发生改变,如果没有发生改变,则跳出更新函数,复用当前fiber节点。如果发生了改变,则调用一个叫propagateContextChange的方法,对该Provider组件的子组件进行深度遍历,找到订阅了当前context的子组件,并打上需要更新的标记,lane。
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const providerType: ReactProviderType<any> = workInProgress.type;
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
pushProvider(workInProgress, context, newValue);
if (enableLazyContextPropagation) {
// In the lazy propagation implementation, we don't scan for matching
// consumers until something bails out, because until something bails out
// we're going to visit those nodes, anyway. The trade-off is that it shifts
// responsibility to the consumer to track whether something has changed.
} else {
if (oldProps !== null) {
const oldValue = oldProps.value;
if (is(oldValue, newValue)) {
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(workInProgress, context, renderLanes);
}
}
}
// do something...
}
那么, 在深度遍历的时候,又是如何知道当前子组件是否有订阅当前Context的呢?其实在使用useContext的时候,除了读取当前context的value,还会把接收的context对象信息保存在当前组件的Fiber.dependencies上,所以在遍历的时候,只需要看当前组件的dependencies上有没有当前context便可以知道当前组件是否存在订阅关系了。
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
lastContextDependency = contextItem;
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}
只有被Provider组件包裹的子组件才能读取到Context的状态吗?其实并不是,所有的组件都可以通过useContext去读取Context对象里的currentValue,但是,只有被Provider组件包裹的组件,才能订阅到Context对象里的value的变化,在变化的时候及时的更新自身组件的状态。这样设计的目的,实际上也是为了更好的优化React在更新组件的性能,试想,如果每创建一个Context对象,就默认所有的组件都可以订阅到这个Context的变化,那么整个Fiber树在更新的过程中,需要遍历的Fiber节点就太庞大了,一些完全不需要且没有订阅当前Context的组件也需要被遍历到,这其实是一种性能的浪费。
转载自:https://juejin.cn/post/7235435351049568312