Context的概念比较容易理解,它的目的就是做组件间数据共享。让嵌套很深的组件不需要一层一层的把props传下来。
我们先看一下如何使用Context
创建Context
我们用React.createContext
来创建Context对象。可以给他传默认值。我们就用React官网的例子1
const MyContext = React.createContext(defaultValue);
当React渲染订阅了这个Context对象的组件,这个组件会从组件树中离自身最近的那个匹配的Provider
中读取到当前的context值。
如果他没有匹配到Provider
也就是没有被Provider
包裹的情况下。将使用defaultValue。
这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
Context.Provider
1 | <MyContext.Provider value={/* 某个值 */}> |
每个Context对象都会返回一个Provider React组件,它允许Consumer订阅context的变化。
Provider接收一个value属性,传递给Consumer组件。一个Provider可以和多个Consumer组件有对应关系。多个Provider也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。
Class.contextType
1 | class MyClass extends React.Component { |
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
当然使用这种方法,你只能挂在一个context。
Context.Consumer
用来订阅context的变更,它让函数式组件可以订阅context。
这种方法,子元素必须是一个函数,这个函数接收context的值,并返回一个React节点。传递给函数的value值等价于组件树上方离这个context最近的Provider提供的value值。如果没有对应的Provider,value参数等同于传递给createContext()的defaultValue。
Context.displayName
context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
注意
官网中的例子,这个一定要注意:
因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:1
2
3
4
5
6
7
8
9class App extends React.Component {
render() {
return (
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
);
}
}
为了防止这种情况,将 value 状态提升到父节点的 state 里:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
对比新老Context
在我最初用Context的时候,官方还是不推荐使用的。说是一般程序没有必要用Context而且Context还在实验阶段将在之后的版本里删除。在新的Context出来之后,现在可以频繁的见到Context的使用。而且在hook出现之后,useContext和useReducer配合还可以代替Redux。
老Context存在的问题。
在16.3之前Context这样用。1
2
3
4
5
6
7
8
9class Parent extends React.Component {
getChildContext() {
return {color: "purple"};
}
}
Parent.childContextTypes = {
color: PropTypes.string
};
然后就可以在任意一级子组件上访问 Context 里的内容了:1
2
3
4
5
6
7
8
9
10
11
12
13class Child extends React.Component {
render() {
return (
<p>
{this.context.color}
</p>
);
}
}
Child.contextTypes = {
color: PropTypes.string
};
用起来也不太复杂。但是和shouldComponentUpdate
搭配起来就有问题。1
2
3
4
5<a>
<b>
<c>
</b>
</a>
其中组件a会通过 getChildContext
设置 Context,组件c通过 this.context
读取 Context。
当a设置新的Context,会触发子组件rerender,然后b会rerender。最终导致c的rerender,并且在 c render的时候会取到新的context。
这一切都很正常,如果这是b中使用了shouldComponentUpdate
的时候,由于b中的state和props没有变,不会导致b的rerender,导致c也不会rerender这时,c就取不到新的Context的值。
新的Context
- Provider 和 Consumer 必须来自同一次 React.createContext 调用。也就是说 A.Provider 和 B.Consumer 是无法搭配使用的。
- React.createContext 方法接收一个默认值作为参数。当 Consumer 外层没有对应的 Provider 时就会使用该默认值。
- Provider 组件的 value prop 值发生变更时,其内部组件树中对应的 Consumer 组件会接收到新值并重新执行 children 函数。此过程不受 shouldComponentUpdete 方法的影响。
- Provider 组件利用 Object.is 检测 value prop 的值是否有更新。注意 Object.is 和 === 的行为不完全相同。
- Consumer 组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。