「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」
今天来聊聊 react的context。 本文内容按以下结构展开:
- 参考资料
- 功能介绍和应用场景
- 从源码中学习context
源码设计的精髓学习感悟
参考资料
功能介绍 & 应用场景
context 是针对 react 组件跨多层组件通信时层层传递 prop 导致代码臃肿繁琐的困境的一种解决方案, 其特点是在某层组件外面包括一个 context 的 Provider,所有后代组件都能消费该 context,并且在传到 Provider 的 value 发生变化的时候能够引发所有消费对应 context 的组件的重渲染。
多个后代组件共同消费一个 context 会导致这些后代组件及 context 的 Provider 之间的耦合,从而降低组件的可复用性。对于 prop,你可以明确知道取值是来自父组件,但对于 context 来说,你不能立即反应过来它的 Provider 放在祖先组件的哪一个位置,为了减少维护的复杂性,最好约定一个规则,比如将所有的 Provider 尽可能放在组件顶层的位置。
context的使用语法无非以下几步:
创建一个 context
const RootContext = React.createContext(null)
放置 Provider
function Root() {
const [provided, setProvided] = useState({})
// ...
const calc = useMemo(() => ({ foo: {...provided, bar: true} }), [provided])
return (
<RootContext.Provider value={calc}>
<App />
</RootContext.Provider>
)
}
此处有个细节,我传递给 Provider 的 calc 是一个用了 useMemo 缓存的值,这是为了避免组件重新渲染时引发Provider不必要的更新(如果直接把{ foo: {...provided, bar: true} }传给 Provider,则会因为在 Provider 浅比较 prop 时认为给 value 传了新的引用,引起所有消费了 RootContext 的后代组件的重渲染)
后代组件使用context
function Sub() {
const rootContext = useContext(RootContext)
console.log(rootContext)
// ...
}
context 的使用场景有:设置主题、读取用户信息、用户偏好设置、设置显示语言、定制组件······ 这些场景的共同特点是多个组件消费同一批数据,且可能会被更新。其他满足这些特点的数据使用场景也可以使用context
从源码中学习context
首先从react github仓库下载源码,为了减少下载的体积,可以直接下载zip包而不是 git clone。
读源码的一个习惯就是先看 package.json, 可以看到
{
"private": true,
"workspaces": [
"packages/*"
],
...
}
react 仓库是一个 monorepo,"workspaces" 指明了工作空间的所有子项目,有关workspaces可以参考Monorepo最佳实践之Yarn Workspaces, 这里只需要知道值得我们关注的源码在packages的某个包中,准确来说就在 packages/react/src中。
这次我们关注 ReactContext.js 和 ReactHooks.js 的两个文件
import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import type {ReactContext} from 'shared/ReactTypes';
export function createContext<T>(defaultValue: T): ReactContext<T> {
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
Provider: (null: any),
Consumer: (null: any),
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
context.Consumer = context;
return context;
}
省略注释和dev环境特供代码,核心逻辑就这么简单。再看看我们的ReactHooks.js中的useContext做了什么
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
export function useContext<T>(Context: ReactContext<T>): T {
const dispatcher = resolveDispatcher();
return dispatcher.useContext(Context);
}
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return ((dispatcher: any): Dispatcher);
}
不讲武德,最终useContext的逻辑是在 packages/react-reconciler/src/ReactFiberHooks.new.js 中
// 已省略大部分代码
import {readContext} from './ReactFiberNewContext.new';
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useContext: readContext,
//...
}
可以看出
export function readContext<T>(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) {
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,
};
if (enableLazyContextPropagation) {
currentlyRenderingFiber.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}
由此看出,useContext的返回值是来自context 对象的_currentValue 或 _currentValue2字段。
下一步我们看看 Provider组件时怎么提供值的,这里可以看看 packages/react-reconciler
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) {
} 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 {
propagateContextChange(workInProgress, context, renderLanes);
}
}
}
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
在这一步,会给value做 diff,如果检测到发生变化,就会执行 propagateContextChange(workInProgress, context, renderLanes); 触发所有消费context的组件的重渲染。
而 Provider 给 context 提供 value 的关键代码是 pushProvider(workInProgress, context, newValue);
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T,
): void {
if (isPrimaryRenderer) {
push(valueCursor, context._currentValue, providerFiber);
context._currentValue = nextValue;
context._currentRenderer = rendererSigil;
}
} else {
push(valueCursor, context._currentValue2, providerFiber);
context._currentValue2 = nextValue;
context._currentRenderer2 = rendererSigil;
}
}
}
这里可以看到我们的新value被赋给了context对象的 _currentValue2字段,