3

抛转引玉

通过上一篇的科普我们知道如果父节点需要向子节点传递数据,那么就得通过Props来实现;那么摆在我们眼前的就有一个问题了:现有N个节点并且它们都是嵌套成父子结构,大致如下

<A>
    <B>
        <C>
            <D>
                ......
                <Last></Last>
            </D>
        </C>
    </B>
</A>

如过last组件需要A组件的某个数据,按照之前的说法我们可以使用Props;但是我觉得一般人都不会这么做,为什么?一个数据在N个组件中通过Props传递,首先写法上会很荣誉、其次就是很可能在某个节点写错了造成最终拿到的数据不是想要的数据,这些都是我们需要考虑的问题。当然有人会想到使用Redux或者Mobx这种第三方库来解决,没毛病;但如果只是一个小小的需求就引入了一个库,是不是杀鸡用了牛刀?在这个问题上React本身有自己的解决方案:Context

Context是什么?

目前React的Context API已经出了两版,在React16.3.0版本之前和之后。实际上我们开发React项目时候很少会用到这个API(至少小编身边是这种情况);而且对于第一版的Context就连官方也不建议用,首先是不好用其次是问题多,不过即使如此不堪的技术却是Redux的基础技术,真的是厉害了!
后来在React16.3.0版本更新之后,全新的Context API与我们见面,可以说是脱胎换骨。官方对Context的介绍是:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

意思就是Context提供了一种通过组件树传递数据的方法,而无需在每个级别手动传递props。可以看出这个技术刚好可以用来解决我们前面提出的问题。

Context可以做什么?

事实上官方设计这个API的目的是共享可被视为React组件树的“全局”数据,例如当前经过身份验证的用户,主题或首选语言。意图言简意赅,可以理解成为React组件树(从Root节点开始通过不断组合各个组件形成最终的树形结构)中注入了一个上下文对象同时将一些全局通用的数据放在这个对象中,这样我们就可以在这个组件树的任何地方使用这些数据。

如何使用Context?

针对新版Context,官方给我们提供了三个API:

  1. React.createContext
  2. Provider
  3. Consumer

通过字面意思大家应该就能猜到它们分别的作用了吧!

React.createContext: 用来创建Context对象
Provider: 用来向组件树发出Context对象
Consumer: 使用Context对象

不过呢,后两者其实是React.createContext创建出来的对象的组成,用一段代码来解释吧:

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

嗯...就酱紫!!!!
其实写到这里我相信用过Redux的朋友就已经开始觉得眼熟了,就是ProvidercreateContext。因为react-redux提供Provider, Redux提供createStore。这也是Redux基于Context API重要物证哈哈....

实例使用Context

学习技术最终是要有产出的。笔者也一步一步来实现一个简单例子,功能:通过点击按钮对屏幕中数字进行加1操作
首先我们需要创建两个js文件:

buildContext.js
import {createContext} from 'react';

const defaultData = {};
export const {Provider, Consumer} = createContext(defaultData);

这里可能有人会有疑问:为什么将创建Context单独抽离出来?
1) 将Context和组件隔离;因为它们不存在必要的联系,Context只是单纯的注入组件而已。
2) 因为Provider, Consumer需要配对使用(注意:Provider, Consumer配对使用的前提是它们都来自同一个createContext);我们可以在Provider下的任意节点使用Consumer,所以就可能存在Provider, Consumer不在同一个组件的情况,所以将将创建Context单独抽离出来使得处理Context更加优雅。

ContextDemo.js
import React, {Component} from 'react'
import {Provider, Consumer} from './buildContext';

class ContextDemo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    addOne = () => {
        this.setState((preState) => ({
                    count: preState.count + 1
                }
            )
        )
    };

    render() {
        return (
            <div>
                <Provider value={this.state}>
                    <div>
                        <Consumer>
                            {
                                (context) => <p>{context.count}</p>
                            }
                        </Consumer>
                    </div>
                </Provider>
                <input type="button" value="加1" onClick={this.addOne}/>
            </div>
        )
    }
}

export default ContextDemo

这里我们重点解释下Provider与Consumer:

Provider
被视作一个React组件,它的作用就是接收一个value属性并把它当做最终Context实体注入到Provider的所有子组件中;同时Provider允许被Consumer订阅一个或多个Context的变动,也就是说Provider内部可以有N个Consumer并且它们都可以拿到最新&&相同的Context对象。

如例子所示,我们将组件的State对象注入到Provider字组件中,如果State发生变化那么Provider中的Context对象必定会同步发生变化。

Consumer
依然被视作一个React组件,不过不同的是它的子组件必须是一个方法并且该方法接收当前Context对象并最终返回一个React节点。同时这里有两个问题需要重点关注:

  1. Consumer接收到的Context对象为离它最近的那个Provider注入的Context对象(且必须是通过value属性)。因为Provider作为一个组件也可以进行嵌套。不过笔者认为单独一个React项目最好只存在一个Context对象而且应该作为一个App级的Context对象(也就是将项目的根节点作为Provider的子组件)。这样做笔者认为有两个好处:1)全局只有一个Context更有利于方便使用和管理;2)作为一个App级的Context对象可以让我们在项目的任何一个地方使用到Context对象,发挥Context最大的力量。
  2. 如果Provider不存在(如果存在那么必须要有value属性,否则报错),那么Consumer获取到的Context对象为最初createContext方法的默认参数。

综上所述:Provider的value == Consumer子组件(function)的入参

当我们理解了这两个概念,我们再回过头来看代码;
我们将组件的State(this.state)通过Provider注入到其子组件中,其实可以预料到当我们更改State时候Context对象也会同步变化最终保持一致。所以:

<Consumer>
    {
        (context) => <p>{context.count}</p>
    }
</Consumer>

此时Consumer的子组件(function)的入参context就可以认为是this.state的复制体,所以可以在方法中获取到相应的数据并且在点击按钮更改了State后Context也发生变化,从而实现UI的重新渲染。

小小的测试

前面有句话说:Provider, Consumer配对使用的前提是它们都来自同一个createContext。因此笔者针对这点做了两个实验,目的是测试当Provider, Consumer不是来自同一个createContext会出现什么情况。这里新建两个文件buildContext.js和ContextTest.js

情况一

buildContext.js
import {createContext} from 'react';

export const {Provider} = createContext({'name': 'Mario'});
export const {Consumer} = createContext({'age': '26'});
ContextTest.js
import React, {Component} from 'react';
import {Provider, Consumer} from "./buildContext";

class Context extends Component {
    render() {
        return (
            <Provider>{/*name*/}
                <Consumer>{/*age*/}
                    {
                        (context) => (
                            <div>
                                <p>age: {context.age}</p>
                                <p>name: {context.name}</p>
                            </div>
                        )
                    }
                </Consumer>
            </Provider>
        )
    }
}

export default Context;

clipboard.png

运行的结果有点意想不到,Consumer拿到的Context并不是离它最近的Provider提供的,而是创造它的createContext方法的默认值,即:export const {Consumer} = createContext({'age': '26'});。再换个写法看看!

情况二

buildContext.js
import {createContext} from 'react';

export const NameContext = createContext({'name': 'Mario'});
export const AgeContext = createContext({'age': '26'});
ContextTest.js
import React, {Component} from 'react';
import {NameContext, AgeContext} from "./buildContext";

class Context extends Component {
    render() {
        return (
            <NameContext.Provider value={{'name':'React'}}>{/*name*/}
                <AgeContext.Consumer>{/*age*/}
                    {
                        (context) => (
                            <div>
                                <p>age: {context.age}</p>
                                <p>name: {context.name}</p>
                            </div>
                        )
                    }
                </AgeContext.Consumer>
            </NameContext.Provider>
        )
    }
}

export default Context;;

这里我们给Provider提供一个value属性;

clipboard.png

运行结果与第一种情况相同;

结论

因此我们可以猜测:如果Provider, Consumer不是来自同一个createContext,那么Consumer获取到的Context则是自己的createContext方法的默认值,此时的Provider被视为不存在。

Context的简单实用就介绍到这里,本篇很多地方都来自笔者的个人看法,如有理解不当烦请丢砖。
同时笔者也尝试写了一个更具有代表性(纯属笔者意淫)的Context应用实例,将Context、ContextHandler、Component分离出来,实现更改相邻组件的样式。麻雀虽小五脏俱全,请各位朋友多多海涵!!
对了项目启动脚本是npm install || npm start


风吹过的夏夜
295 声望31 粉丝

前端工程师