Context
https://juejin.cn/post/7244838033454727227?searchId=202404012120436CD549D66BBD6C542177
context 提供了一个无需为每层组件手动添加 props, 就能在组件树间进行数据传递的方法
React 中数据通过 props 属性自上而下(由父及子)进行传递,但此种用法对于某些类型的属性而言极其繁琐(UI 主题,地区偏好),这些属性时应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显示的通过组件树逐层传递 props
-
创建 context
-
使用 context 的 Provider 进行包裹
-
使用 contextType 进行接收,this.context 读取
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</hemeContext.Provider>
)
}
}
function Toolbar() {
return (
<div><ThemeButton /></div>
)
}
class ThemeButton extends React.Component {
// static contextType = ThemeContext
return <Button theme={this.context} />
}
ThemedButton.contectType = ThemeContext
function ThemedButton() {
return (
<ThemeContext.Consumer>
{(value) => <button>{value}</button>}
</ThemeContext.Consumer>
);
}
context 用于不同层级的组件需要访问同样的数据。使得组件的复用性变差
如果只是想避免层层传递的一些属性,可以使用component composition
- 将组件本身作为 props 传递下去
- 这种对组件的控制减少了应用中需要传递的 props 数量,在很多场景下会使得代码更加干净,但是并不适用于每一个场景,这种将逻辑提升到组件树的更高层次会使得高层组件变得更加复杂,并且会强行将底层组件适应这样的样式
API
React.createContext
const MyContext = React.createContext(defaultValue);
创建一个 Context 对象,当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
Context.Provider
<MyContext.Provider value="" />
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化
Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 可以嵌套使用,里层的会覆盖外层的数据
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(contextType, useContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其组件跳过更新的情况下也能更新。
生产消费者模型
组件只要定义了 contextType, 就是消费者,消费者可以订阅生产者,消费者可以随时响应状态的变化
context 可以无视中间组件的渲染,依然可以响应生产者数据的变化
通过新旧值检测来确定变化,使用了和 Object.is 相同的算法
=0 === +0 // true
Object.is(-0, +0) // false
注意点
- context 会根据引用标识来决定何时进行渲染(本质是 value 属性值的比较)
- 当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染
- 当每一次 Provider 重渲染时,由于 value 属性总是被赋值为新的对象,以下的代码会重新渲染所有的 consumer 组件
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={{ something: "something" }}>
<Toolbar />
</MyContext.Provider>
);
}
}
原理
- 将初始值存储在 context._currentValue
- 创建 Context.Provider 和 Context.Consumer 对应的 ReactElement 对象
在 fiber 树渲染时,通过不同的 workInProgress.tag 处理 Context.Provider 和 Context.Consumer 类型的节点。
在 React 中提供了 3 种消费 Context 的方式
- 直接使用 Context.Consumer 组件(也就是上面 createContext 时创建的 Consumer)
- 类组件中,可以通过静态属性 contextType 消费 Context
- 函数组件中,可以通过 useContext 消费 Context
这三种方式内部都会调用 prepareToReadContext 和 readContext 处理 Context。prepareToReadContext 中主要是重置全局变量为 readContext 做准备。
readContext 的核心逻辑:
- 构建 contextItem 并添加到 workInProgress.dependencies 链表(contextItem 中保存了对当前 context 的引用,这样在后续更新时,就可以判断当前 fiber 是否依赖了 context ,从而判断是否需要 re-render)
- 返回对应 context 的 _currentValue 值
更新 Context
当触发 Context.Provider 的 re-render 时,重新走 updateContextProvider 中更新的逻辑
核心逻辑:
- 从 ContextProvider 的节点出发,向下查找所有 fiber.dependencies 依赖当前 Context 的节点
- 找到消费节点时,从当前节点出发,向上回溯标记父节点 fiber.childLanes,标识其子节点需要更新,从而保证了所有消费 3. 了该 Context 的子节点都会被重新渲染,实现了 Context 的更新
总结
在消费阶段,消费者通过 readContext 获取最新状态,并通过 fiber 关联当前 Context
在更新阶段,从 ContextProvider 节点出发查找所有消费了该 context 的节点