前言
Context被翻译为上下文,在编程领域,这是一个经常会接触到的概念,在 React 的官方文档中,Context被归类为高级部分(Advanced),属于React的高级API。下面就来说一说 React 中的 context。
需要明确的是,本文中提到的Context API都是指【新版Context API】。新版Context API 是在 react 16.3 版本所引入的。在这个版本之前的 context API我们称之为旧版 context API。它们都是为了解决同样的问题:“跨组件层级传递props的效率问题(prop-drilling)”。
Context的官方定义
React文档官网并未对Context给出“是什么”的定义,更多是描述使用的Context的场景,以及如何使用Context。
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
简单理解就是,当不想在组件树中通过逐层传递props或者state的方式来传递数据时,可以使用Context来实现跨层级的组件数据传递。
使用Context,可以跨越组件进行数据传递。
如何使用 Context
使用Context API遵循下面三个步骤:
- 首先,调用
const ThemeContext = React.createContext()去创建一个context 对象实例; - 其次,使用
<ThemeContext.Provider value={someValue}>组件去声明你想要传递的任何数据; - 最后,通过render props范式
<ThemeContext.Consumer>{(someValue)=> ....}</ThemeContext.Consumer>或者hook的写法const theContextValue = useContext(ThemeContext)来把想要读取的数据取回来再进行消费。
在新版Context API的实现里面,只要某个子组件订阅了context对象实例所承载的数据,只要这些数据发生了改变,不管该子组件的上层组件做了什么,这个子组件都会得到更新。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。 // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事, // 因为必须将这个值层层传递所有组件。 return ( <div>
<ThemedButton theme={props.theme} />
</div> );
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
使用 context, 我们可以避免通过中间元素传递 props:
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
把 Context 当做组件作用域
使用React的开发者都知道,一个React App本质就是一棵React组件树,每个React组件相当于这棵树上的一个节点,除了App的根节点,其他每个节点都存在一条父组件链。
例如上图,
<Child />的父组件链是<SubNode /> -- <Node /> -- <App />,<SubNode />的父组件链是<Node /> -- <App />,<Node />的父组件链只有一个组件节点,就是<App />。
这些以树状连接的组件节点,实际上也组成了一棵Context树。
有了解JS作用域链概念的开发者应该都知道,JS的代码块在执行期间,会创建一个相应的作用域链,这个作用域链记录着运行时JS代码块执行期间所能访问的活动对象,包括变量和函数,JS程序通过作用域链访问到代码块内部或者外部的变量和函数。
假如以JS的作用域链作为类比,React组件提供的Context对象其实就好比一个提供给子组件访问的作用域,而Context对象的属性可以看成作用域上的活动对象。由于组件的Context由其父节点链上所有组件通过getChildContext()返回的Context对象组合而成,所以,组件通过Context是可以访问到其父组件链上所有节点组件提供的Context的属性。
使用 Context 之前的考虑
作为React的高级API,React并不推荐我们优先考虑使用Context
, Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。
比如,考虑这样一个 Page 组件,它层层向下传递 user 和 avatarSize 属性,从而深度嵌套的 Link 和 Avatar 组件可以读取到这些属性:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
如果在最后只有 Avatar 组件真的需要 user 和 avatarSize,那么层层传递这两个 props 就显得非常冗余。而且一旦 Avatar 组件需要更多从来自顶层组件的 props,你还得在中间层级一个一个加上去,这将会变得非常麻烦。
一种无需 context 的解决方案是将 Avatar 组件自身传递下去,因而中间组件无需知道 user 或者 avatarSize 等 props:
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}
这种变化下,只有最顶部的 Page 组件需要知道 Link 和 Avatar 组件是如何使用 user 和 avatarSize 的。
这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。
而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)”,正如这里的文档所列举的
function Page(props) {
const user = props.user;
const content = <Feed user={user} />;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
</NavigationBar>
);
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}
这种模式足够覆盖很多场景了,在这些场景下你需要将子组件和直接关联的父组件解耦。如果子组件需要在渲染前和父组件进行一些交流,你可以进一步使用 render props。
但是,有的时候在组件树中很多不同层级的组件需要访问同样的一批数据。Context 能让你将这些数据向组件树下所有的组件进行“广播”,所有的组件都能访问到这些数据,也能访问到后续的数据更新。使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据,这比替代方案要简单的多。
总结
-
相比
props和state,React的Context可以实现跨层级的组件通信。 -
Context API的使用基于生产者消费者模式。Provider 接收一个
value属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。 -
使用
Context需要多一些思考,不建议在App中使用Context,但如果开发组件过程中可以确保组件的内聚性,可控可维护,不破坏组件树的依赖关系,影响范围小,可以考虑使用Context解决一些问题。 -
Context只考虑偏静态数据的跨组件层级传递和共享,不考虑状态更新。 -
可以把
Context当做组件的作用域来看待,但是需要关注Context的可控性和影响范围,使用之前,先分析是否真的有必要使用,避免过度使用所带来的一些副作用。 -
可以把
Context当做媒介,进行App级或者组件级的数据共享。