「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」
react 版本:v17.0.3
1、入口
在 React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:
-
组件挂载阶段:
useContext = ReactCurrentDispatcher.current.useContext = HooksDispatcherOnMount.useContext = readContext;
-
组件更新阶段:
useContext = ReactCurrentDispatcher.current.useContext = HooksDispatcherOnUpdate.useContext = readContext;
无论是挂载阶段还是更新阶段,useContext 最终执行的函数都是 readContext 。接下来,我们来看看 readContext 的实现。
2、readContext
// packages/react-reconciler/src/ReactFiberHooks.new.js
export function readContext<T>(context: ReactContext<T>): T {
// 删除了Dev部分代码
// 以下两个属性是为了适配多平台(浏览器端/移动端)
// _currentValue
// _currentValue2
// ReactDOM 中 isPrimaryRenderer 为 true,定义的就是 true
// 实际就是一直会返回 context._currentValue
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
// 新建一个 context 链表的节点,节点上存储着传递进来的 context 对象 和 context 对象上的value
// next 指针连接下一个 context 项
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
// 删除了部分代码
// This is the first dependency for this component. Create a new list.
// 这是组件的第一个依赖项,创建一个新的 context 依赖列表
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
currentlyRenderingFiber.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
// 在链表后面添加一个新的 context 项
lastContextDependency = lastContextDependency.next = contextItem;
}
}
// readContext最终返回的是context._currentValue
return value;
}
readContext 把 context 对象上的 _currentValue/_currentValue2 取出来,接着构建一个新的 context项,该 context 项上存储着当前的 context 对象和 context 对象上的 _currentValue/_currentValue2,并通过 next 指针连接下一个 context 项,接着构建一个 context 依赖列表,并将该列表挂载到当前正在渲染的 Fiber 节点,最后返回从 context 对象上取出来的 _currentValue/_currentValue2 。
readContext 接收一个 context 对象 (React.createContext 的返回值) 并返回该 context 的当前值。我们接下来看看这个 context 对象和该 context 的当前值。
3、createContext
React 的 Context 属性实现了组件间跨层级传递 props 。Context 对象通过 createContext 方法创建:
const MyContext = React.createContext(defaultValue);
我们来看看 createContext 的实现:
// packages/react/src/ReactContext.js
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> = {
// ReactContext中的$$typeof是作为createElement中的属性type中的对象进行存储的
$$typeof: REACT_CONTEXT_TYPE,
// 作为支持多个并发渲染器的解决方法,我们将一些渲染器分类为主要渲染器,将其他渲染器分类为辅助渲染器。
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary.
// 我们只希望最多有两个并发渲染器:React Native(主要)和Fabric(次要);
// React DOM(主要)和React ART(次要)。
// 辅助渲染器将自己的context的value存储在单独的字段中。
// 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.
// <Provider value={xxx}>中的value就是赋值给_currentValue的
// 也就是说_currentValue和_currentValue2作用是一样的,只是分别给主渲染器和辅助渲染器使用
_currentValue: defaultValue, // Provider 的value 属性
_currentValue2: defaultValue, // Provider 的value 属性
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_threadCount: 0, // 用来追踪 context 的并发渲染器数量
// These are circular
Provider: (null: any), // 提供组件
Consumer: (null: any), // 应用组件
};
// 给context对象添加 Provider 属性,并且 Provider 中的_context指向的是 context 对象
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
let hasWarnedAboutUsingNestedContextConsumers = false;
let hasWarnedAboutUsingConsumerProvider = false;
let hasWarnedAboutDisplayNameOnConsumer = false;
if (__DEV__) {
// 删除了 DEV 部分的代码
} else {
// 也就是Consumber对象指向React.Context对象
// 在<Consumer>进行渲染时,为了保证Consumer拿到最新的值,
// 直接让Consumer=React.Context,
// React.Context中的_currentValue已经被<Provider>的value给赋值了
// 所以Consumer能立即拿到最新的值
context.Consumer = context;
}
// 删除了 DEV 部分的代码
return context;
}
在 createContext 中,构建一个 context 对象,将传递进来的 defaultValue 赋值给 context 对象的 _currentValue 和 _currentValue2 属性,并在 context 对象上定义了一个用来追踪 context 并发渲染器数量的 _threadCount 属性,一个为 Consumer组件提供 value 的 Provider组件,和一个用于消费 context 的 Consumer组件。
_currentValue 和 _currentValue2 两个属性是为了适配不同的平台,如 Web端、移动端。这两个属性在 context 对象初始化时都会赋值为传入的 defaultValue 。在 React 更新的过程中,会一直有一个叫做 valueCursor 的栈,这个栈可以帮助记录当前的 context,每次更新组件的时候,_currentValue 和 _currentValue2 都会被赋值为最新的value(该部分内容暂不展开讲解,后续另开一篇文章详细讲解)。
context 对象构建好之后,就将当前的 context 对象分别挂载到 Provider 组件和 Consumer 组件上。
最后将该 context 对象返回,其数据结构如下:
4、使用注意事项
因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:
class App extends React.Component {
render() {
return (
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
);
}
}
为了防止这种情况,将 value 状态存储到父节点的 state 里:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<MyContext.Provider value={this.state.value}>
<Toolbar />
</MyContext.Provider>
);
}
}