详解react的跨层级通信Context

2,441 阅读5分钟

context典型应用场景

context,顾名思义就是上下文的意思, 在一个典型的react应用中,数据是通过props属性自上而下(由父及子)传递的,但这种做法对于某些属性而言是很繁琐的(例如UI主题),比如平时应用的主题色是蓝色,某次节日需要更换主题色,大到页面,小到孙子组件里面的button都得换色。此时若在最顶层定义然后往下逐层去传递的话非常繁琐。这个时候使用组件得跨层级通信比较方便,context提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props。

react使用context实现祖代组件向后代组件跨层级传值,类似于vue中的provide/inject。

React.createContext

创建一个Context对象。当React渲染一个订阅了这个Context对象的组件,这个组件会从组件树中离自身最近的那个匹配的Provider中读取到当前的context值。

React.Provider

Provider接收一个value属性,传递给消费组件,允许消费组件订阅context的变化。一个Provider可以和多个消费组件有对应关系。多个Provider也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发⽣变化时,它内部的所有消费组 件都会重新渲染。Provider 及其内部 consumer 组件都不受 制于 shouldComponentUpdate 函数,因此当 consumer 组 件在其祖先组件退出更新的情况下也能更新。

Class.ContextType

挂载在 class 上的 contextType 属性会被重赋值为⼀个由 React.createContext() 创建的 Context 对象。这能让你 使⽤ this.context 来消费最近 Context 上的那个值。你可 以在任何⽣命周期中访问到它,包括 render 函数中。

你只通过该 API 订阅单⼀ context。

Context.Consumer

这⾥,React 组件也可以订阅到 context 变更。这能让你在函 数式组件中完成订阅 context。 开课吧web全栈架构师 这个函数接收当前的 context 值,返回⼀个 React 节点。传 递给函数的 value 值等同于往上组件树离这个 context 最近 的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。

具体使用Context

消费单个Context

以实现共享主题色需求为例。

步骤:创建Context => 获取Provider和Consumer => Provider提供 值 => Consumer消费值。

import React, { Component } from "react";
import { ThemeProvider } from "../themeContext";
import ContextTypePage from "./ContextTypePage";
import ConsumerPage from "./ConsumerPage";
class ContextPage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: {
        themeColor: "red",
      },
    };
  }
 
  changeColor = () => {
    const { themeColor } = this.state.theme;
    this.setState({
      theme: {
        themeColor: themeColor === "red" ? "green" : "red",
      },
    });
  };
  render() {
    const { theme } = this.state;
    return (
      <div className="App">
        {/* 组件跨层级通信 */}
        <button onClick={this.changeColor}>change color</button>
        {/* 如果把这⾥的MyProvider注释掉,
        ContextTypePage和ConsumerPage⾥将取不到theme值,⽽
        取默认值pink */}
        <ThemeProvider value={theme}>
          <ContextTypePage />
          <ConsumerPage />
        </ThemeProvider>
      </div>
    );
  }
}
export default ContextPage;

 

themeContext.js

import React from "react";

// 创建context 农民种菜, 如果没有匹配到Provider,取值默认值
export const ThemeContext = React.createContext({themeColor: "pink"});
// 接收者 批发商批发菜
export const ThemeProvider = ThemeContext.Provider;

//消费者 吃菜
export const ThemeConsumer = ThemeContext.Consumer;

ContextTypePage.js

import React, { Component } from "react";
import { ThemeContext } from "../themeContext";
export default class ContextTypePage extends Component {
  static contextType = ThemeContext;
  render() {
    console.log("ContextTypePage", this.context); //
    const { themeColor } = this.context;
    return (
      <div className="border">
        <h3 className={themeColor}>ContextTypePage</h3>
      </div>
    );
  }
}

ConsumerPage.js

import React, { Component } from "react";
import { ThemeConsumer } from "../themeContext";
export default class ConsumerPage extends Component {
  render() {
    return (
      <div className="border">
        <h3>ConsumerPage</h3>
        <ThemeConsumer>{(ctx) => <HandleTabBar {...ctx} />}</ThemeConsumer>
      </div>
    );
  }
}
function HandleTabBar({ themeColor }) {
  console.log("themeColor", themeColor); //

  // 补充ContextPage.js
  // pages/MultipleContextPage.js
  return <div className={themeColor}>⽂本</div>;
}

消费多个Context

补充ContextPage.js

<ThemeProvider value={theme}>
 <ContextTypePage />
 <ConsumerPage />
 {/*多个Context */}
 <UserProvider value={user}>
 <MultipleContextsPage />
 </UserProvider>
 </ThemeProvider>

MultipleContextPage.js

import React, { Component } from "react";
import { ThemeConsumer } from "../themeContext";
import { UserConsumer } from "../userContext";
export default class MultipleContextsPage extends Component {
  render() {
    return (
      <div className="border">
        <h3>MultipleContextsPage</h3>
        <ThemeConsumer>
          {(theme) => (
            <UserConsumer>
              {(user) => <div className={theme.themeColor}>{user.name}</div>}
            </UserConsumer>
          )}
        </ThemeConsumer>
      </div>
    );
  }
}

如果两个或者更多的 context 值经常被⼀起使⽤,那你可能要考虑⼀下另外创建你⾃⼰的渲染组件,以提供这些值。

注意事项

因为 context 会使⽤参考标识(reference identity)来决定 何时进⾏渲染,这⾥可能会有⼀些陷阱,当 provider 的⽗组 件进⾏重渲染时,可能会在 consumers 组件中触发意外的渲 染。举个例⼦,当每⼀次 Provider 重渲染时,以下的代码会 重渲染所有下⾯的 consumers 组件,因为 value 属性总是被赋值为新的对象:

class App extends React.Component {
  render() {
    return (
      <Provider value={{ something: "something" }}>
        <Toolbar />
      </Provider>
    );
  }
}

为了防⽌这种情况,将 value 状态提升到⽗节点的 state ⾥:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: { something: "something" },
    };
  }
  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

小结

在React的官⽅⽂档中,Context被归类为⾼级部分 (Advanced),属于React的⾼级API,但官⽅并不建议在稳定 版的App中使⽤Context。

不过,这并⾮意味着我们不需要关注Context。事实上,很 多优秀的React组件都通过Context来完成⾃⼰的功能,⽐如 react-redux的,就是通过Context提供⼀个 全局态的store,路由组件react-router通过Context管理路 由状态等等。在React组件开发中,如果⽤好Context,可以 让你的组件变得强⼤,⽽且灵活。

函数组件中可以通过useContext引⼊上下⽂,后⾯hooks 部分介绍。