Context 上下文

上下文:Context,表示做某一些事情的环境 React 中的上下文特点:

  1. 当某个组件创建了上下文后,上下文中的数据,会被所有后代组件共享
  2. 如果某个组件依赖了上下文,会导致该组件不再纯粹(纯粹指的是外部数据仅来源于属性 props)
  3. 一般情况下,用于第三方组件(通用组件)

旧的 API

创建上下文

只有类组件才可以创建上下文

  1. 给类组件书写静态属性 childContextTypes,使用该属性对上下文中的数据类型进行约束
  2. 添加实例方法 getChildContext,该方法返回的对象,即为上下文中的数据,该数据必须满足类型约束,该方法会在每次 render 之后运行。

使用上下文中的数据

要求:如果要使用上下文中的数据,组件必须有一个静态属性 contextTypes,该属性描述了需要获取的上下文中的数据类型。

  1. 可以在组件的构造函数中,通过第二个参数,获取上下文数据。但是由于构造函数只会运行一次,后面上下文数据改变了,不会更新
  2. 从组件的 context 属性中获取
  3. 在函数组件中,通过第二个参数,获取上下文数据。数据并不会流动异常,只是调用了父组件的函数而已

    创建上下文只能是类组件,获取上下文可以是类组件或函数组件

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(默认值)创建

返回的是一个包含两个属性的对象

  1. Provider 属性:生产者。一个组件,该组件会创建一个上下文,该组件有一个 value 属性,通过该属性,可以为其数据赋值
    1. 同一个 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}
  1. Consumer 属性:后续讲解

使用上下文中的数据

  1. 在类组件中,直接使用 this.context 获取上下文数据
    1. 要求:必须拥有静态属性 contextType , 应赋值为创建的上下文对象
  2. 在函数组件中,需要使用 Consumer 来获取上下文数据
    1. Consumer 是一个组件
    2. 它的子节点,是一个函数(它的 props.children 需要传递一个函数)
    3. 不需要写静态属性

      使用函数组件获取上下文

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 的可靠性需要关注