zoukankan      html  css  js  c++  java
  • 从零开始的react入门教程(九),react context上下文详解,可能有点啰嗦,但很想让你懂

    壹 ❀ 引

    我在从零开始的react入门教程(八),redux起源与基础用法一文中,介绍了redux的前辈Flux,以及redux关于单项数据更新的基本用法。我们在前文提到,相对Flux支持多个storeredux推荐唯一数据源,也就是使用一个全局Store去掌管所有数据。数据源虽然统一了,但我们要使用Store还是得把Store引入到需要的组件中,比如上文中的Counter组件与Summary组件,毕竟使用dispatch或者监听Store变化都离不开这个数据源,但这就造就了两个问题。

    问题一,假设我们有多个组件都依赖了Store数据,组件分布在不同文件夹,或者说我们使用的三方库也依赖了此数据,用一处就得引一次,文件路径的相对关系都是一个不小的麻烦。

    问题二,可能有同学就想到,哎,react不是有个概念叫状态提升吗,大不了我在顶层组件引用一次,通过props进行数据传递,但这样就会造成多个组件其实并不需要这份数据,但为了子孙组件能顺利访问数据,都成了数据传递的搬运工。

    针对上面两个问题,我们其实可以通过Context得以解决,Context顾名思义就是上下文。就像在一个作用域内我们提前声明了一个变量,后续代码不需要再做引用操作,你都能直接访问它,Context的作用也是如此。

    我在整理Context资料的时候发现了一个问题,由于react版本原因,react对于Context的解释也是存在历史变迁的。作为一个初学者,如果你在百度想搜Context用法然后发现了不同的介绍,估计你也会纳闷我到底应该用哪种(或者对于直接上手react-redux的同学可能根本没了解过原生Context的用法),这里我先做个简单的总结,在react版本16.X之前,Context的使用依赖childContextTypes对象,然后手动定义Provider组件,比如在《深入浅出react和Redux》一书中,代码例子的react版本还是15.4.1,所以书中介绍的自然是前面提到的做法。而对于现在的版本比如官方文档中,Context的使用已经不需要手动定义Provider组件了,而是createContext方法手动创建,用法上会人性化很多。

    本文还是会站在不同的两个版本,去介绍它们的用法,以达到解决文章开头关于Store引用与传递的问题,当然,如果你已经确定了当前项目的react版本,你可以自由选择对应的版本文档了解其用法。

    如果可以,我还是希望有缘看到这篇文章的人能跟着手敲代码,感受其具体的用法,那么本文开始。

    贰 ❀ Context 旧版(版本16.X之前)

    说在前面,下面的代码仍然基于上一篇文章的例子修改,当然如果没有代码,我尽可能将使用上的细节描述清楚(当然我还是推荐跟着例子来)。如果大家有简单了解过Context,脑海里一定对Provider的单词有所印象,不过对于老版本而言,我们并不能直接引用并使用它,而是需要自己创建,确实非常尴尬。

    我们现在src目录下新建一个Provider.js的文件,里面的代码为:

    import {Component} from 'react';
    import PropTypes from 'prop-types';
    
    class Provider extends Component {
      getChildContext() {
        // 我们会通过store字段将全局store传递进来
        return {
          store: this.props.store
        };
      }
    	// 渲染Provoder所包裹的子组件内容
      render() {
        return this.props.children;
      }
    }
    
    Provider.propTypes = {
      store: PropTypes.object.isRequired
    }
    
    Provider.childContextTypes = {
      store: PropTypes.object
    };
    
    export default Provider;
    
    

    这段代码有几点需要拧出来说,第一个是关于PropTypes,写过react的同学都知道这是做组件属性的类型检查,比如我一个组件哪些属性是必须提供,哪些是字符串等等。这个东西呢其实也存在一个历史问题....早期版本的react,是可以直接通过引用拿到此对象然后使用,比如:

    import { PropTypes } from 'react';
    

    但是在react 15.5之后,此属性被react官方废弃掉了,如果你是版本比较高的react,像上面这样引用会告诉你PropTypesundefied并报错,比如我参考的《深入浅出react和Redux》一书中都是这么用的,因为作者例子的react版本也比较低(15.4.1),而我在写demo的react版本已经是16了,自然用不了,不过也没有关系,咱们可以通过如下方式引用PropTypes

    import PropTypes from 'prop-types';
    

    prop-types是一个独立的三方库,因此我们需要提前安装这个包,比如执行命令yarn add prop-types,若你是npm请执行npm i prop-types,这里就不多介绍了,关于prop-types后续也可能会专门写一篇用法的文章。

    回到上面的代码中,Provider组件定义的内容其实非常简单,一个getChildContext方法,用于创建子组件的上下文,而上下文中包含的东西其实也就是我们需要使用的store数据,this.props.store怎么来下面的代码会交代。除此之外还有一个render方法,用于渲染Provider包裹的子组件。关于this.props.children这里做个简单补充,比如我们有一个父组件A与一个子组件B,A包裹B,如下:

    import React, { Component } from 'react';
    import ReactDOM from 'react-dom';
    function A(props) {
        console.log(props);
        return <div>我是父组件{props.children}</div>
    }
    function B() {
        return <div>我是子组件</div>
    }
    
    ReactDOM.render(
        <A><B/></A>,
        document.getElementById('root')
    );
    

    可以看到我们使用了A包裹了B,在A组件的返回中,我们通过{props.children}成功拿到了包裹的B组件,并将其渲染了出来,通过控制台输出也看的很明显,这里的chindren属性其实就是组件A所包含的组件内容。

    我们再过分点,直接修改为如下代码:

    ReactDOM.render(
        <A>
            {
                <div>
                    <div>1</div>
                    <div>2</div>
                </div>
            }
        </A>,
        document.getElementById('root')
    );
    

    再看控制台,你会发现通过children属性,我们先访问到了包裹的最外层的div,然后此div的children又是一个数组,因为它又包含了两个div,继续再通过children属性,我们就可以找到数组第一个元素的孩子是一个数字1,这就是react中children的作用,在实际开发中,我们也常会利用此属性达到组件父子组件嵌套的目的。

    OK,题外话说完了,再回到上述代码,注意如下这段代码:

    Provider.childContextTypes = {
      store: PropTypes.object
    };
    

    这段代码是必须提供的,不然直接报错,它的类型定义与getChildContext方法中提供的类型相对应,它用于告诉react我现在为子组件提供了一个上下文,上下文中包含的数据有哪些,每个属性是什么类型,关于Provider.js先说到这里。

    在上一篇文章的例子中,我们通过index.js文件最终渲染了所有组件,这里我们需要做些修改,具体如下:

    import React, { Component } from 'react';
    import ReactDOM from 'react-dom';
    import store from './Store.js';
    import Provider from './Provider.js';
    import Counter from './Counter.js';
    import Summary from './Summary.js';
    class ControlPanel extends Component {
        render() {
            return (
                <Provider store={store}>
                    <div>
                        <Counter caption="First" />
                        <Counter caption="Second" />
                        <hr />
                        <Summary />
                    </div>
                </Provider>
            );
        }
    }
    
    ReactDOM.render(
        <ControlPanel />,
        document.getElementById('root')
    );
    

    我们在此文件中引用了前面定义的Provider组件,同时也引用了全局的Store,然后通过Provider组件将上篇文章中需要渲染的组件进行了包裹,同时通过store字段将引用过来的store作为props传递了下去,这里就对应了Provider.jsgetChildContext方法this.props.store的来源。

    上述的修改其实很好理解,我们将Provider作为顶层组件,为需要渲染的所有组件提供了一个共有的上下文,而这个上下文中存在一个store属性,也就是全局的Store,现在子组件们不需要再分别引用Store.js文件了,但这些子组件还需要做一些改变才能支持访问上下文。

    Counter组件为例,这里我们说下需要修改的几个点:

    import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    import * as Actions from './Actions.js';
    
    class Counter extends Component {
      constructor(props,context) {
        super(props,context);
        // 初始化组件的state
        this.state = this.getOwnState();
      }
    
      getOwnState = () => {
        // 这里的this.props.caption其实就是前面说的First Second
        return {
          // 这里可以拿到当前的Store数据,并根据key取到对应的初始值
          value: this.context.store.getState()[this.props.caption]
        };
      }
    
      onIncrement = () => {
        // Actions.increment返回的其实是一个action对象,注意这个函数其实只传递了一个参数,也就是上面提到的First Second类型
        this.context.store.dispatch(Actions.increment(this.props.caption));
      }
    
      onDecrement = () => {
        this.context.store.dispatch(Actions.decrement(this.props.caption));
      }
      // 用于更新state
      onChange = () => {
        this.setState(this.getOwnState());
      }
    
      shouldComponentUpdate(nextProps, nextState) {
        // 如果state的value变了,通知组件更新
        return nextState.value !== this.state.value;
      }
    
      componentDidMount() {
        // 监听Store变化,Store变了我们就让组件的state也跟着变
        this.context.store.subscribe(this.onChange);
      }
    
      componentWillUnmount() {
        this.context.store.unsubscribe(this.onChange);
      }
    
      render() {
        const { value } = this.state;
        const { caption } = this.props;
    
        return (
          <div>
            <button onClick={this.onIncrement}>+</button>
            <button onClick={this.onDecrement}>-</button>
            <span>{caption} count: {value}</span>
          </div>
        );
      }
    }
    // 这里必须定义,不然访问不到Context
    Counter.contextTypes = {
      store: PropTypes.object
    }
    export default Counter;
    

    第一点就是我们同样引入了PropTypes,因为在代码最下面,我们必须定义contextTypes的类型,这里与Provider.js中的childContextTypes定义其实是对应的,上下文在创建的时候定义了,子组件在引用上下文时同样得做一个定义声明。

    第二点,在constructor中我们知道super方法用于子组件在初始化时继承父组件传递的属性,而这里我们得额外添加一个context,表示将上下文传递进来。

    第三点,之前在Counter中我们直接引入了Store.js,因此可以直接访问store的数据以及API方法,但此时我们是通过上下文访问,因此需要对之前所有使用到store的前面添加上this.context,具体可参照上述代码。同理,我们将Summary组件中也做上述三点修改,然后执行yarn start运行项目,你会发现非常完美,项目成功跑起来了。

    那么到这里,我们通过旧版的Context做法取代了传统Store引用的做法,达到了只在index.js一处引用统一管理,并可在所有子组件中访问此上下文的目的。

    叁 ❀ Context 新版(版本16.X之后)

    其实对前面旧版的修改写下来,你会发现这玩意还真不是那么好用,虽说不用每个组件引入Store了吧,咱还得自己手写Provider组件不说,每个用到store的组件还得专门定义contextTypes的类型,实属有点麻烦。没事,我们继续来看新版的Context的用法。当然这次,至少咱们不用手写Provider组件了。

    在对于新版本Context资料查阅中,我看到了一句对于Context作用描述比较精准的话,那就是Context能实现组件跨层级的数据传递。比如Props传递一定是逐层的,这可能就会对一些不需要这部分数据的组件造成感染,那么我想越级传递,中间的组件不需要感知这部分数据的存在,Context就是一个不错的渲染。当然回到上文,我们还是可以理解为Context为相关联的组件提供了一个共有上下文,子可见后代也可见,那么就不需要子帮忙传递后代都可以拿着用。因此,除了应对全局Store的数据传递之外,某些部分组件的数据越级传递(比如数据与Store无关,单纯几个层级关系组件之间需要做传递),以及部分子组件,后代组件都需要访问到父组件的部分数据,其实都可以使用此做法达到目的

    OK,新版Context的几个核心概念为createContextProviderConsumer,我们一个个说。

    叁 ❀ 壹 createContext

    createContext顾名思义,创建一个上下文也就是Context对象,它的一般用法为:

    const context = React.createContext();
    

    而这个创建出来的context对象中,又包含了ProviderConsumer两个组件,输出如下:

    因此在使用时,其实也可以像下面这样直接获取到两个组件:

    const {Provider, Consumer} = React.createContext();
    

    createContext可以接受一个参数defaultValue,表示我在创建这个上下文时,就默认定义了一部分的共有的数据,但这个默认数据生效是有条件的,这里引用官方文档的描述:

    createContext创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的 Provider 中读取到当前的 context 值。而如果当前组件所处的组件树中都没有匹配到Provider是,这时候defaultValue就会生效。

    怎么理解呢?也就是说我们在父组件创建了一个上下文,但后代组件中只用了Consumer组件,而没有使用Provider对应提供数据,那这时候相当于处于保护措施,我们让defaultValue生效,保证Consumer能拿到默认的数据,免得组件渲染报错了,实属吃低保的行为了。关于这部分的例子,可以参阅React.createContext point of defaultValue?的问题回复,因为这部分知识又涉及到了hookuseContext,简单理解就是父组件中createContext创建上下文,而在子组件中可以使用useContext解析context中的数据,这里我们先不细谈。

    叁 ❀ 贰 Provider

    故名思域,与旧版我们定义的Provider作用大致相同,它用于包裹需要享有相同上下文的所有组件,以及为其提供上下文中共有的数据,但需要注意的是,这里的数据传递必须通过value字段,比如:

    <Provider value={/*需要传递的共享数据*/}>
        /*被包裹的组件们*/
    </Provider>
    

    多个Provider可以嵌套使用,但是里层的Provider的value会覆盖掉外层的Provider的value,因此Consumer访问context注定是访问距离自己最近的Provider。除此之外还有一点,当Provider传递的value发生了变化时,Provider内部的所有Consumer组件都会被强制重新渲染,shouldComponentUpdate这玩意都不会限制住它,目的是保证所有消费者组件永远同步感知最新的context变化。

    叁 ❀ 叁 Consumer

    如名称理解的那样,消费者,也就是消费(使用)Provider传递下来数据的组件。正常情况下,Consumer组件得嵌套在Provider组件之下,但如果如上面所说我们没用Provider组件只用了Consumer组件,那么Consumer组件能访问的上下文就是在createContext中定义的defaultValue

    基本API都介绍了,我们来通过这种方式再来改写我们前面的例子。

    首先,我们在src目录下新建一个Context.js文件,代码如下:

    import React from 'react';
     
    const context = React.createContext();
     
    export default context;
    

    之后,在index.js文件引入context,这里直接再贴上代码:

    import React, { Component } from 'react';
    import ReactDOM from 'react-dom';
    import store from './Store.js';
    import Counter from './Counter.js';
    import Summary from './Summary.js';
    import context from './Context.js';
    class ControlPanel extends Component {
        render() {
            return (
              	//我们使用了Provider包裹子组件,通过value传递store
                <context.Provider value={store}>
                    <div>
                        <Counter caption="First" />
                        <Counter caption="Second" />
                        <hr />
                        <Summary />
                    </div>
                </context.Provider>
            );
        }
    }
    ReactDOM.render(
        <ControlPanel />,
        document.getElementById('root')
    );
    

    同理,我们再次修改Counter组件,还是直接上代码:

    import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    import * as Actions from './Actions.js';
    import context from './Context.js';
    // const context = React.createContext();
    class Counter extends Component {
      // static contextType = context;
      constructor(props,context) {
        super(props,context);
        // 初始化组件的state
        this.state = this.getOwnState();
      }
    
      getOwnState = () => {
        // 这里的this.props.caption其实就是前面说的First Second
        return {
          // 这里可以拿到当前的Store数据,并根据key取到对应的初始值
          value: this.context.getState()[this.props.caption]
        };
      }
    
      onIncrement = () => {
        // Actions.increment返回的其实是一个action对象,注意这个函数其实只传递了一个参数,也就是上面提到的First Second类型
        this.context.dispatch(Actions.increment(this.props.caption));
      }
    
      onDecrement = () => {
        this.context.dispatch(Actions.decrement(this.props.caption));
      }
      // 用于更新state
      onChange = () => {
        this.setState(this.getOwnState());
      }
    
      shouldComponentUpdate(nextProps, nextState) {
        // 如果state的value变了,通知组件更新
        return nextState.value !== this.state.value;
      }
    
      componentDidMount() {
        // 监听Store变化,Store变了我们就让组件的state也跟着变
        this.context.subscribe(this.onChange);
      }
    
      componentWillUnmount() {
        this.context.unsubscribe(this.onChange);
      }
    
      render() {
        const { value } = this.state;
        const { caption } = this.props;
    
        return (
          <div>
            <button onClick={this.onIncrement}>+</button>
            <button onClick={this.onDecrement}>-</button>
            <span>{caption} count: {value}</span>
          </div>
        );
      }
    }
    Counter.contextType = context;
    export default Counter;
    

    因为我们需要在Counter组件使用context,因此也需要引入context。之后,我们通过Counter.contextType = context;为当前组件绑定context对象,同理,在constructor中还是得初始化context,之后在组件任意地方,我们都可以通过this.context访问到传递进来的store,注意啊,这里的this.context已经等同于store本身了,所以代码中是this.context.subscribe直接调用store上的API。你可能有点不习惯,还是希望this.context.store去访问,那就像如下方式这样传递,比如假设我们需要给Provider传递多个值:

    class ControlPanel extends Component {
        render() {
            const value = {
                store,
                name:1
            };
            return (
                <context.Provider value={value}>
                    <div>
                        <Counter caption="First" />
                        <Counter caption="Second" />
                        <hr />
                        <Summary />
                    </div>
                </context.Provider>
            );
        }
    }
    

    我们再去Counter断点this,你就发现这就是你预期的样子了

    其实可以发现,新版的context在使用上与旧版还是有些类似的,在使用context的地方同样得为组件做contextType的定义以及context的初始化,我们同理去修改掉Summary中的代码,执行运行项目的命令,你会发现也能完美跑起来,那么到这里,我们又通过新版Context的做法修改了例子。

    当然到这里我们还没用到Consumer,那么接下来我们再单独用一个例子,再次结合把ProviderConsumer用一用。接下来我们定义ABC三个组件,A嵌套B,B又嵌套C,直接修改index.js中的代码:

    import React, { Component } from 'react';
    import ReactDOM from 'react-dom';
    import context from './Context.js';
    class A extends Component {
        render() {
            const name = '听风是风';
            return (
                <context.Provider value={name}>
                    <div>{`我是A组件,我传递了${name}`}</div>
                    {/* 注意,这里我们并没有将name作为props传递下去 */}
                    <B />
                </context.Provider>
            )
        }
    }
    function B() {
        return (
            <context.Consumer>
                {
                    (name) => {
                        console.log(name);
                        return (
                            <div>
                                {`我是B组件,我接受了${name}`}
                                <C />
                            </div>
                        )
                    }
                }
    
            </context.Consumer>
        )
    }
    function C() {
        return (
            <context.Consumer>
            {
                (name)=>{
                    return (
                        <div>
                            {`我是C组件,我接受了${name}`}
                        </div>
                    )
                }
            }
            </context.Consumer>
        )
    }
    ReactDOM.render(
        <A />,
        document.getElementById('root')
    );
    

    可以看到,在子组件需要使用context的地方,我们通过context.Consumer将其包裹,而context.Consumer之间接受一个函数,此函数接受一个参数(参数随便你叫什么),此参数就是Provider的映射,比如我们上面传递的是一个字符串,注意,只有一层花括号进行了包裹,所以函数形参name直接就是所传递值的映射。

    那假设我们传递了多个参数呢?还是一样,我们稍作修改,这里只贴上修改的部分,并在子组件函数中尝试打印:

    class A extends Component {
        render() {
            const name = '听风是风';
            const age = '28';
            return (
                <context.Provider value={{name,age}}>
                    <div>{`我是A组件,我传递了${name}`}</div>
                    {/* 注意,这里我们并没有将name作为props传递下去 */}
                    <B />
                </context.Provider>
            )
        }
    }
    
    function B() {
        return (
            <context.Consumer>
                {
                    // 参数其实可以随便你取名
                    (aaa) => {
                        console.log(aaa);
                        return (
                            <div>
                                {`我是B组件,我接受了${aaa.name}`}
                                <C />
                            </div>
                        )
                    }
                }
    
            </context.Consumer>
        )
    }
    

    当然实际开发中,我们不会推荐这样传递多个参数,因为上述代码中value={{name,age}}部分,代码每次执行{name,age}可以理解为每次都是一个全新的对象,由于对象引用不同这会导致react认为value每次都在发生变化,从而引发子组件全部更新,推荐的做法是使用一个变量去声明一个对象包含这两个变量,比如:

    // 这里只贴主要修改部分
    const user = {
        name:'听风是风',
        age:28
    }
    return (
        <context.Provider value={user}>
            <div>{`我是A组件,我传递了${user.name}`}</div>
            {/* 注意,这里我们并没有将name作为props传递下去 */}
            <B />
        </context.Provider>
    )
    
    <context.Consumer>
        {
            (user) => {
                return (
                    <div>
                        {`我是B组件,我接受了${user.name}`}
                        <C />
                    </div>
                )
            }
        }
    
    </context.Consumer>
    

    那么到这里,我们其实展示了两种在子组件中访问context的方式,第一种是为组件绑定contextType,第二种就是使用Consumer,那么我们直接将C组件修改成如下的方式:

    class C extends Component {
        constructor(props, context) {
            super(props, context)
        }
        render() {
            return (
                <div>
                    {`我是C组件,我接受了${this.context}`}
                </div>
            )
        }
    }
    C.contextType = context;
    

    可以看到我们没有借用Consumer,而是借用组件contextType绑定后,同样成功访问到了父组件传递的数据。

    那么到这里,我们介绍了react中新旧context的基本用法,旧版context需要自定义Provider,并结合getChildContext定义为子组件传递的数据。而新版context在使用上相对友好了不少,我们可以通过createContext创建一个context实例,并可以直接使用Provider提供数据,使用Consumer消费数据。通过文中新旧例子对比,其实两者在使用上存在不少相同点。

    在下一篇文章中,我们来了解react-redux基本用法,其实本篇文章与上一篇文章属于react-redux的铺垫篇,在了解了react原生的概念后,我想在理解三方封装时应该会容易很多,那么到这里本文结束。

    参考

    深入浅出react和Redux 第三章组件context部分

    React Context(上下文) 作用和使用 看完不懂 你打我

    React系列——React Context

    react官网文档Context

    React中Context的使用

    React context基本用法

  • 相关阅读:
    大数据之路week07--day05 (一个基于Hadoop的数据仓库建模工具之一 HIve)
    大数据之路week07--day04 (Linux 中查看文件内容的关键字处)
    大数据之路week07--day04 (YARN,Hadoop的优化,combline,join思想,)
    hdu 1575 Tr A(矩阵快速幂,简单)
    hdu 1757 A Simple Math Problem (矩阵快速幂,简单)
    zoj 2974 Just Pour the Water (矩阵快速幂,简单)
    LightOj 1065
    LightOj 1096
    poj 1006 生理周期(中国剩余定理)
    POJ 2251 Dungeon Master(广搜,三维,简单)
  • 原文地址:https://www.cnblogs.com/echolun/p/14948149.html
Copyright © 2011-2022 走看看