还有一点时间才下班,今天晚上先把这篇博客的头开一下。实际上这节课的是比较早就看过的课程了,应该在参加工作之前吧,但是过了这么久早就已经忘了,虽然知道自己已经看过,但是对我没有一点实质性的帮助。于是借助前一段在家休假的时间,把这部分的内容重新看了一遍,这回再用博客的方式复一下盘,算是做一个了结吧。

在初学 React 的时候,就知道 props 可以用来实现父子,乃至于更深层级之间的组件通信。但是如果组件树的结构非常复杂,使用 props 来实现通信就非常不方便。在之前的博客里面也提到过使用 EventHub 来实现组件通信,但是如果只是想为组件树创建一个能够共同访问的值,用 EventHub 又未免太过麻烦。全局变量虽然可以再全局环境下访问到,但是并不够稳妥,需要用局部变量。Context 的目的就是共享对于一个组件树而言是全局的数据。

树叶

Context 通过组建树提供了一个传递数据的方法,从而避免了各个层级手动传递 props 属性。使用 props 从上往下传递数据,对于情形极其繁琐。Context 使得不必通过组件树的每个层级显式地传递 props。下面是一个简单的例子:

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

除了使用static contextType读取 context 的值之外,但是这种方法只能在类组件中使用。更为通用的方法是,可以使用Context.Consumer一样可以让组件订阅到 context 的改变。下面是一个例子,这里的Button组件可以通过 props 的方式获取到 context 的值:

const ThemedButton = () => {
    return (
      <ThemeContext.Consumer>
        {value => <Toolbar context={value} />}
      </ThemeContext.Consumer>
    );
}

这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给createContext()的 defaultValue。

这种标签里面套函数的做法看起来很奇怪,因为平时很少这样做,更多的时候会在标签里面套函数的调用。不过这样做也是不会报错的,在标签里套函数不会渲染任何内容,但是组件中套的函数可以通过props.children取到。如果组件中有多个子组件,props.children就是数组。而此处的 Consumer 就是调用了props.children,并以 context 的值作为函数的参数,render 函数的返回值。上面的操作实际上等同于:

const ThemedButton = () => {
    return React.createElement(ThemeContext.Consumer, null, 
        value => React.createElement(Toolbar, { context: value }));
}

所有的 JSX 元素都是调用 React.createElement(component, props, ...children) 的语法糖。因此,使用纯 JavaScript 可以完成 JSX 能做到的任何事情。

为了让更深层次的子组件也能更新 context,可以把函数作为 context 的值传递给子组件,函数中则包括 setState 等相关操作。在下面的例子中,context 把需要传递的值连带着修改的方法放在一个对象中传递给了下层组件:

const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

function ThemeTogglerButton() {
  // Theme Toggler 按钮不仅仅只获取 theme 值,它也从 context 中获取到一个 toggleTheme 函数
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>

          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // State 也包含了更新函数,因此它会被传递进 context provider。
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // 整个 state 都被传递进 provider
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

ReactDOM.render(<App />, document.root);

在之前的博客中也提到过,React Hooks 中的 useContext 也可以让函数组件方便地订阅 context,效用与static contextType相似,看起来比Context.Consumer更方便,至少不用再标签里套函数了。这里就不再赘述了:

const ThemedButton = () => {
    const theme = useContext(ThemeContext);
    return (
        <Toolbar context={theme} />}
    );
}

以上就是本篇博客中关于 React Context API 的所有内容。回想刚刚开始学 React 的时候,那是还觉得这是比较难的知识,虽然也看过了相关的课程,但是并没有学进去,过一会就忘记了。现在回头看看感觉也就是那么回事,果然实践才是最好的老师,如果没有天天工作中使用 React 经验,就算是今天写完博客,还是一知半解也说不定。

实践

本篇博客实际上就是过了一遍 React Context API 的文档,这里贴一个链接吧: