React中的上下文属性:Context

React有一个很重要的设计思想:单向数据流,数据通过props自上而下的传递,所以每一个接收prop方注定要有一个传送方,这并没什么,但是,在某些场景下,多个不同层级的组件需要共享一个值,并且有可能会对该值进行修改就会变得非常的繁琐。

React组件传输数据

1. Context属性

React考虑到这种情况,暴露了一个Context对象。

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

Context 能让你将这些数据向组件树下所有的组件进行“广播”,所有的组件都能访问到这些数据,也能访问到后续的数据更新。使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据,这比替代方案要简单的多。

2. How to use

React.createContext

使用React.createContext创建Context对象,当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值(这个过程是React帮我们完成的)。

为了解构更加清晰,我们创建一个contextManager的js文件作为管理入口。

import React from 'react';
// 1. 创建context对象,暴露一个txt属性和修改txt属性的方法,不用在乎它们此时的值,此时的值都是默认值
const Manager = React.createContext({
    txt: "hi~",
    changeTxt:()=>{}
});
export default Manager

接下来就是对 Context 对象的使用了。

Context.Provider

Provider也就是提供者,换言之就是在你需要传递数据的组件源头进行Provider的包装,然后context中的数据就会由此处向下进行传递。
我们选在的在App.jsx组件树的总源头进行包装:

import React, { Component } from "react";
import { Switch, Route } from "react-router-dom";
import "./index.scss";
import Index from "./pages/index";
import Order from "./pages/Order";
import EventHandle from "./pages/EventHandle";
// 引入context管理文件
import Manager from "./pages/contextManager"

class App extends Component {
    constructor(props) {
        super(props);
        // do something
        this.changeTxt = (newTxt) =>{
            this.setState({
                txt: newTxt
            })
        }
        // 
        this.state = {
            txt: "I miss you", // 重新定义context对象中的txt属性
            changeTxt: this.changeTxt, // 重新定义context对象中的changeTxt方法
        }
    }
    render() {
        return (
            // 使用Provider包装,使用value属性向下传递数据
            <Manager.Provider value={this.state}>
                    <Switch>
                        <Route path="/event" component={EventHandle} />
                        <Route path="/order" component={Order} />
                        <Route path="/" component={Index} />
                    </Switch>
            </Manager.Provider>
        );
    }
}

export default App

这样顶层的传输已经完成,接下来是子组件的接收过程。

Class.contextType

首先将context对象使用Class.contextType的方式挂载到Class组件上,这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

当然这个挂载过程非常简单,赋值一下即可。

...
// 引入context对象
import Manager from "../contextManager";

class Clock extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            date: new Date(),
        };
        this.handleChangeTxt;
    }

    componentDidMount() {
        console.log("this", this.context); // 此时context就可以使用了
        // 接入改变context.txt的方法,this.context.changeTxt
        this.handleChangeTxt = this.context.changeTxt;
    }

    componentWillUnmount() {}

    render() {
        return (
            <div>
                <div onClick={()=>{this.handleChangeTxt("me too")}}>
                    {this.context.txt}
                    <h1>Hello, world!</h1>
                </div>
            </div>
        );
    }
}
// 给Class组件挂载context对象
Clock.contextType = Manager;

export default Clock;

通过contextType的属性挂载context对象后,就可以在组件内部接收顶层组件传输过来的数据了。

Context.Consumer

React 也提供了另外一种方式访问Context对象,一般使用在函数式组件当中,因为从contextType的挂载中可以看到Class组件已经可以使用this.context进行访问和更改了。

在函数式组件中,我们可以使用Context.Consumer包装,进而访问context

import Manager from "../contextManager";
function Welcome(props) {
    return <Manager.Consumer>
    {
        value=>{ // 此时value的值就是context对象,其中包含一个txt属性和一个changeTxt方法
            return <div>
                Hello, {value.txt}
            </div>
        }
    }
    </Manager.Consumer>
}

这样,在函数式组件中也可以访问到由顶层父组件传递下来的context值了,这个过程也一样会查找最近的传输点(Provider)。

3. 注意

为了避免context的变更引起不必要的渲染,Provider提供的value数据最好提取到Provider的state里

class App extends Component {
    constructor(props) {
        super(props);
        this.changeTxt = (newTxt) =>{
            this.setState({
                txt: newTxt
            })
        }
        this.state = {
            txt: "I miss you",
            changeTxt: this.changeTxt
        }
    }
    render() {
        return (
            // GOOD!
            <Manager.Provider value={this.state}>
                ...
            </Manager.Provider>
        );
    }
    /*
      render() {
        return (
            // BAD!!
            <Manager.Provider value={{data: 'barbarFooFoo'}}>
                ...
            </Manager.Provider>
        );
    }
    */
}

export default App

引用官方文档的解释:

因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码(<Manager.Provider value={{data: 'barbarFooFoo'}}>)会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象。

到此,Context的使用就已经大致介绍完毕了,其他的诸如消费多个 Context等,只需要创建多个Context对象,然后在顶层入口处按照先后顺序嵌套一下即可,就像使用DOM节点的嵌套一样。


1 + 9 =

求知若飢,虛心若愚。