Context 上下文
上下文:Context,表示做某一些事情的环境
React 中的上下文特点:
- 当某个组件创建了上下文后,上下文中的数据,会被所有后代组件共享
- 如果某个组件依赖了上下文,会导致该组件不再纯粹(纯粹指的是外部数据仅来源于属性 props)
- 一般情况下,用于第三方组件(通用组件)
旧的 API
创建上下文
只有类组件才可以创建上下文
- 给类组件书写静态属性 childContextTypes,使用该属性对上下文中的数据类型进行约束
- 添加实例方法 getChildContext,该方法返回的对象,即为上下文中的数据,该数据必须满足类型约束,该方法会在每次 render 之后运行。
使用上下文中的数据
要求:如果要使用上下文中的数据,组件必须有一个静态属性 contextTypes,该属性描述了需要获取的上下文中的数据类型。
- 可以在组件的构造函数中,通过第二个参数,获取上下文数据。但是由于构造函数只会运行一次,后面上下文数据改变了,不会更新
- 从组件的 context 属性中获取
- 在函数组件中,通过第二个参数,获取上下文数据。数据并不会流动异常,只是调用了父组件的函数而已
创建上下文只能是类组件,获取上下文可以是类组件或函数组件
1import React, { Component } from "react";
2import PropTypes from "prop-types";
3const types = {
4 a: PropTypes.number,
5 b: PropTypes.string.isRequired,
6};
7function ChildA(props, context) {
8 // 在函数组件中,通过第二个参数,获取上下文数据
9 return (
10 <div>
11 <h1>ChildA</h1>
12 <h2>
13 a{context.a} ; b{context.b}
14 </h2>
15 <ChildB />
16 </div>
17 );
18}
19ChildA.contextTypes = types; //前提条件contextTypes
20
21class ChildB extends React.Component {
22 static contextTypes = types;
23 // 方法2:从组件的context属性中获取
24 constructor(props, context) {
25 super(props, context);
26 console.log(this.context);
27 }
28 // 如果不写构造函数,会自动运行super
29 render() {
30 return (
31 <p>
32 ChildB 来自于上下文的数据 a:{this.context.a}, b:{this.context.b}
33 </p>
34 );
35 }
36}
37export default class OldContext extends Component {
38 /**
39 * 约束上下文中数据的类型
40 */
41 static childContextTypes = types;
42 /**
43 *
44 * @returns 得到上下文数据
45 */
46 getChildContext() {
47 console.log("获取上下文中的数据");
48 return {
49 a: 123,
50 b: "aaa",
51 };
52 }
53 render() {
54 return (
55 <div>
56 <ChildA />
57 </div>
58 );
59 }
60}
上下文的数据变化
上下文中的数据不可以直接变化,最终都是通过状态改变
1export default class OldContext extends Component {
2 /**
3 * 约束上下文中数据的类型
4 */
5 static childContextTypes = types;
6 state = {
7 a: 123,
8 b: "abc",
9 };
10 /**
11 *
12 * @returns 得到上下文数据
13 */
14 getChildContext() {
15 console.log("获取新的上下文");
16 return {
17 a: this.state.a, //来自状态了
18 b: this.state.b,
19 };
20 }
21 render() {
22 return (
23 <div>
24 <ChildA />
25 <button
26 onClick={() => {
27 this.setState({
28 //每次改变状态都会重新运行getChildContext。会导致重新渲染
29 a: this.state.a + 1,
30 });
31 }}
32 >
33 a+1
34 </button>
35 </div>
36 );
37 }
38}
在上下文中加入一个处理函数,可以用于后代组件更改上下文的数据
1import React, { Component } from "react";
2import PropTypes from "prop-types";
3const types = {
4 a: PropTypes.number,
5 b: PropTypes.string.isRequired,
6 onChangeA: PropTypes.func,
7};
8function ChildA(props, context) {
9 return (
10 <div>
11 <h1>ChildA</h1>
12 <h2>
13 a{context.a} ; b{context.b}
14 </h2>
15 <ChildB />
16 </div>
17 );
18}
19ChildA.contextTypes = types; //前提条件contextTypes
20
21class ChildB extends React.Component {
22 static contextTypes = types;
23 constructor(props, context) {
24 super(props, context);
25 console.log(this.context);
26 }
27 render() {
28 return (
29 <p>
30 ChildB 来自于上下文的数据 a:{this.context.a}, b:{this.context.b}
31 <button
32 onClick={() => {
33 this.context.onChangeA(this.context.a + 2);
34 }}
35 >
36 子组件按钮a+2
37 </button>
38 </p>
39 );
40 }
41}
42export default class OldContext extends Component {
43 static childContextTypes = types;
44 state = {
45 a: 123,
46 b: "abc",
47 };
48 getChildContext() {
49 return {
50 a: this.state.a,
51 b: this.state.b,
52 onChangeA: (newA) => {
53 //这里要用箭头函数,否则报错
54 this.setState({
55 a: newA,
56 });
57 },
58 };
59 }
60 render() {
61 return (
62 <div>
63 <ChildA />
64 <button
65 onClick={() => {
66 this.setState({
67 a: this.state.a + 1,
68 });
69 }}
70 >
71 a+1
72 </button>
73 </div>
74 );
75 }
76}
新版 API
旧版 API 存在严重的效率问题,并且容易导致滥用
创建上下文
上下文是一个独立于组件的对象,该对象通过 React.createContext(默认值)创建
返回的是一个包含两个属性的对象
- Provider 属性:生产者。一个组件,该组件会创建一个上下文,该组件有一个 value 属性,通过该属性,可以为其数据赋值
- 同一个 Provider,不要用到多个组件中,如果需要在其他组件中使用该数据,应该考虑将数据提升到更高的层次
使用类组件获取上下文
1import React, { Component } from "react";
2
3const ctx = React.createContext(); //里面可以填默认值
4
5// console.log(ctx)
6
7function ChildA(props) {
8 return (
9 <div>
10 <h1>ChildA</h1>
11 <ChildB />
12 </div>
13 );
14}
15
16class ChildB extends React.Component {
17 static contextType = ctx;
18 render() {
19 return (
20 <div>
21 ChildB,来自上下文的数据:a:{this.context.a},b:{this.context.b}
22 <button
23 onClick={() => {
24 this.context.changeA(this.context.a + 2);
25 }}
26 >
27 后代组件的按钮,点击a+2
28 </button>
29 </div>
30 );
31 }
32}
33export default class NewContext extends Component {
34 state = {
35 a: 0,
36 b: "abc",
37 changeA: (newA) => {
38 this.setState({
39 a: newA,
40 });
41 },
42 };
43 /* render() {
44 const Provider = ctx.Provider;//可以把ctx.Provider直接当组件用<ctx.Provider></ctx.Provider>
45 return (
46 <Provider value={this.state}>
47 <div></div>
48 </Provider>
49 )
50 } */
51 render() {
52 return (
53 <ctx.Provider value={this.state}>
54 <div>
55 <ChildA />
56 <button
57 onClick={() => {
58 this.setState({
59 a: this.state.a + 1,
60 });
61 }}
62 >
63 父组件的按钮a+1
64 </button>
65 </div>
66 </ctx.Provider>
67 );
68 }
69}
- Consumer 属性:后续讲解
使用上下文中的数据
- 在类组件中,直接使用 this.context 获取上下文数据
- 要求:必须拥有静态属性 contextType , 应赋值为创建的上下文对象
- 在函数组件中,需要使用 Consumer 来获取上下文数据
- Consumer 是一个组件
- 它的子节点,是一个函数(它的 props.children 需要传递一个函数)
- 不需要写静态属性
使用函数组件获取上下文
1const ctx = React.createContext();
2function ChildA(props) {
3 return (
4 <div>
5 <h1>ChildA</h1>
6 <h2>
7 {/*
8 写法1
9 <ctx.Consumer>
10 这里已经明确使用哪个上下文了,不用写静态属性了
11 {value => <>{value.a},{value.b}</>}
12 </ctx.Consumer> */}
13 {/* 写法2 */}
14 <ctx.Consumer
15 children={(value) => (
16 <>
17 {value.a},{value.b}
18 </>
19 )}
20 ></ctx.Consumer>
21 </h2>
22 <ChildB />
23 </div>
24 );
25}
类组件中,也可以通过这个方法获取。比较常用,不需要 contextType 了
1class ChildB extends Component {
2 render() {
3 return (
4 <ctx.Consumer>
5 {(value) => {
6 <p>
7 来自上下文的数据:{value.a}
8 <button onClick={value.changeA(value.a + 2)}>
9 后代组件按钮,点击a+2
10 </button>
11 </p>;
12 }}
13 </ctx.Consumer>
14 );
15 }
16}
创建多个,互不干扰
1import React, { Component } from "react";
2
3const ctx1 = React.createContext();
4const ctx2 = React.createContext();
5
6function ChildA(props) {
7 return (
8 <ctx2.Provider
9 value={{
10 a: 789,
11 c: "hello",
12 }}
13 >
14 <div>
15 <h1>ChildA</h1>
16 <h2>
17 <ctx1.Consumer>
18 {(value) => (
19 <>
20 {value.a},{value.b}
21 </>
22 )}
23 </ctx1.Consumer>
24 </h2>
25 <ChildB />
26 </div>
27 </ctx2.Provider>
28 );
29}
30
31class ChildB extends React.Component {
32 render() {
33 return (
34 <ctx1.Consumer>
35 {(value) => (
36 <>
37 <p>
38 ChildB,来自于上下文的数据:a: {value.a}, b:{value.b}
39 <button
40 onClick={() => {
41 value.changeA(value.a + 2);
42 }}
43 >
44 后代组件的按钮,点击a+2
45 </button>
46 </p>
47 <p>
48 <ctx2.Consumer>
49 {(val) => (
50 <>
51 来自于ctx2的数据: a: {val.a}, c:{val.c}
52 </>
53 )}
54 </ctx2.Consumer>
55 </p>
56 </>
57 )}
58 </ctx1.Consumer>
59 );
60 }
61}
62
63export default class NewContext extends Component {
64 state = {
65 a: 0,
66 b: "abc",
67 changeA: (newA) => {
68 this.setState({
69 a: newA,
70 });
71 },
72 };
73 render() {
74 return (
75 <ctx1.Provider value={this.state}>
76 <div>
77 <ChildA />
78
79 <button
80 onClick={() => {
81 this.setState({
82 a: this.state.a + 1,
83 });
84 }}
85 >
86 父组件的按钮,a加1
87 </button>
88 </div>
89 </ctx1.Provider>
90 );
91 }
92}
注意细节
如果,上下文提供者(Context.Provider)中的 value 属性发生变化(Object.is 比较),会导致该上下文提供的所有后代元素全部重新渲染,无论该子元素是否有优化(无论 shouldComponentUpdate 函数返回什么结果)
上下文的应用场景
编写一套组件(有多个组件),这些组件之间需要相互配合才能最终完成功能
比如,我们要开发一套表单组件,使用方式如下
1render(){
2 return (
3 <Form onSubmit={datas=>{
4 console.log(datas); //获取表单中的所有数据(对象)
5 /*
6 {
7 loginId:xxxx,
8 loginPwd:xxxx
9 }
10 */
11 }}>
12 <div>
13 账号: <Form.Input name="loginId" />
14 </div>
15 <div>
16 密码: <Form.Input name="loginPwd" type="password" />
17 </div>
18 <div>
19 <Form.Button>提交</Form.Button>
20 </div>
21 </Form>
22 );
23}
对 React context 的理解
在 React 中,数据传递一般使用 props 传递数据,维持单向数据流,这样可以让组件之间的关系变得简单且可预测,但是单项数据流在某些场景中并不适用。单纯一对的父子组件传递并无问题,但要是组件之间层层依赖深入,props 就需要层层传递显然,这样做太繁琐了。
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
可以把 context 当做是特定一个组件树内共享的 store,用来做数据传递。简单说就是,当你不想在组件树中通过逐层传递 props 或者 state 的方式来传递数据时,可以使用 Context 来实现跨层级的组件数据传递。
JS 的代码块在执行期间,会创建一个相应的作用域链,这个作用域链记录着运行时 JS 代码块执行期间所能访问的活动对象,包括变量和函数,JS 程序通过作用域链访问到代码块内部或者外部的变量和函数。
假如以 JS 的作用域链作为类比,React 组件提供的 Context 对象其实就好比一个提供给子组件访问的作用域,而 Context 对象的属性可以看成作用域上的活动对象。由于组件 的 Context 由其父节点链上所有组件通 过 getChildContext()返回的 Context 对象组合而成,所以,组件通过 Context 是可以访问到其父组件链上所有节点组件提供的 Context 的属性。
为什么 React 并不推荐优先考虑使用 Context?
- Context 目前还处于实验阶段,可能会在后面的发行版本中有很大的变化,事实上这种情况已经发生了,所以为了避免给今后升级带来大的影响和麻烦,不建议在 app 中使用 context。
- 尽管不建议在 app 中使用 context,但是独有组件而言,由于影响范围小于 app,如果可以做到高内聚,不破坏组件树之间的依赖关系,可以考虑使用 context
- 对于组件之间的数据通信或者状态管理,有效使用 props 或者 state 解决,然后再考虑使用第三方的成熟库进行解决,以上的方法都不是最佳的方案的时候,在考虑 context。
- context 的更新需要通过 setState()触发,但是这并不是很可靠的,Context 支持跨组件的访问,但是如果中间的子组件通过一些方法不影响更新,比如 shouldComponentUpdate() 返回 false 那么不能保证 Context 的更新一定可以使用 Context 的子组件,因此,Context 的可靠性需要关注