React 学习系列(九): Context

3,129 阅读5分钟

概述

在一个典型的 React 应用 中,数据是通过 props 属性自上而下(由父及子) 进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间 共享 此类值的方式,而不必 显式地通过组件树的逐层传递 props

用法及解析

Context 的使用,简单来说就是 父组件 提供一个 Context 对象子组件(及子组件的子组件等) 使用。当 Context 对象 的值发生变化时,使用 Context 对象子组件(及子组件的子组件等) 触发 更新重新渲染

Context 的使用分为 三步:

  1. 构建 Context 对象

    要在 react 应用 中使用 Context,我们需要先使用 react 提供的 createContext 方法构建一个 Context 对象, 如下:

    const Const = React.createContext();
    

    Context 对象 中有三个 关键属性,他们依次为 _currentValueProviderConsumer

    Context._currentValue, 对应的值为 供子组件使用的 context,默认为 undefined。 在使用 createContext 时,我们可以传入一个 初始值_currentValue 会使用传入的初始值初始化。

    在子组件中使用的 context 值,都是从 Context._currentValue 获取的。

    Context.Providerreact 组件,用于更新 Context._currentValue 并通知使用 Context 对象子组件 更新。

    Context.Consumerreact 组件,用于将 Context._currentValue 传递给 子组件订阅 Context._currentValue 的变化

    订阅, 即 Context._currentValue 发生变化,使用 Context 的子组件都需要更新,重新渲染。

  2. 父组件将 Context 对象引入 react 应用供子组件使用

    Context 对象 构建完成以后,接下来要做的就是将 Context 对象 引入 react 应用子组件 订阅。

    通过 Context.Provider, 我们可以将 Context 对象 引入 react 应用,如下:

    <Context.Provider value={value}>
        <Child />
    </Context.Provider>
    

    Provider 接收一个 value 属性, value 属性 的值会用于 更新 Context._currentValue。 当 value 的值发生变化时,会通知 所有使用 Context 的子组件更新

  3. 子组件获取 Context 对象的值使用

    最后,我们就需要在 子组件 中获取 Context._currentValue 的值使用了。

    获取 Context._currentValue 的方式有如下几种:

    • contextType

      通过给 类组件 定义一个 静态属性 - contextType,我们可以在 类组件实例 中通过 context 属性 来访问 Context._currentValue 的值。

      具体用法如下:

      import Context from './context.js'
      class Child extends React.Component {
          static contextType = Context
          render() {
              return <div>{this.context}</div>
          }
      }
      

      类组件 在渲染过程中,会先读取 Context._currentValue 的值,然后使用读取的 context 值初始化组件实例的 context 属性

      contextTye 方式 局限性比较大,缺点如下:

      1. 仅适用于类组件

      2. 每次只能使用一个 Context 对象

    • useContext

      通过 react 提供的 useContext hook, 我们可以在 函数式组件 中访问 Context._currentValue 的值。

      具体用法如下:

      import Context1 from './context1.js'
      import Context2 from './context2.js'
      function Child(props) {
          const context1 = React.useContext(Context1)
          const context2 = React.useContext(Context2)
          
          return ...
      }
      

      useContext hook 的返回值就是 Context._currentValue

      对比 contextType 方式useContext 方式 可以使用 多个 Context 对象, 但 仅适用于函数式组件

    • Consumer 订阅

      除了使用 contextType 方式useContext 方式, 我们还可以使用 Consumer 方式 实现在 子组件 中访问 Context._currentValue 的值。

      具体用法如下:

      import Context1 from './context1.js'
      import Context2 from './context2.js'
      
      <Context1.Consumer>
          // context1 为 Context1._currentValue
          {context1 => (
              <Context2.Consumer>
                  // context2 为 Context2._currentValue
                  {context2 => <Child context1={context1} context2={context2} /}
              </Context2.Consumer>
          )}
      </Context1.Consumer>
      

      Consumer 方式 适用于 函数式组件类组件,也可以使用 多个Context 对象

    当我们通过 contextTypeuseContextConsumer 方式为 子组件 获取 Context._currentValue 值时,会 订阅 Context 对象

    组件渲染 过程中, react 会为每一个 组件 构建一个对应的 虚拟节点 - fiber node 对象。如果 子组件 通过 contextTypeuseContextConsumer 方式获取 Context._currentValue, 那么对应的 Context 对象 就会保存到 fiber node 对象dependencies 列表 中。

    父组件 修改 Context.Providervalue 属性 的值时,会触发 Context.Provier 组件 的更新。 在更新过程中,会 依次遍历所有子组件虚拟节点的 dependencies 列表,查看 dependencies 中是否包含当前 Context 对象。如果有,强制子组件更新重新渲染,获取最新 Context._currentValue

    注意,上述三种方式,都需要被 Context.Provier 包裹,否则无效

具体应用

Context 在实际的 react 应用 中是有大量使用的,最常见的就是 react-reduxreact-router-dom

  • react-redux

    react-redux 可以帮助我们在 react 应用 中使用 Redux

    通过 react-redux,我们可以在组件中很方便的访问 store 对象state, 并通过 store.dispatch 修改 state,然后通知使用 store.state 的组件更新。

    应用 react-redux 时,需要访问 store 的组件必须包裹在 react-redux 提供的 Provider 组件 中。

    ReactRedux.Provider 在实现过程中使用了 Context 。 它会利用 React.createContext 构建一个 Context 对象Redux 构建的 store 对象 会保存到 Context._currentValue,然后通过 Context.ProviderContext 对象 引入 react 应用 供子组件订阅。被 ReactRedux.Provider 包裹的所有 容器组件(或者使用 hook 的组件) 都可以从 Context 中获取 store 对象

  • react-router-dom

    通过 react-router-dom, 我们可以建立 路由和组件(页面)之间的映射关系。当 切换路由 时,将 匹配的组件(页面) 渲染到对应的位置

    react-router-dom 在实现过程中也使用了 Context

    react-router 深入学习 中我们可以了解到,在应用 react-router-dom 时,会先渲染 BrowserRouter(HashRouter) 组件, 然后渲染 Router 组件,最后渲染 Route 组件。渲染 Router 组件 时,会构建组件实例并初始化 state.location。 当 页面跳转 时,通过 setState 方法更新 Router 组件实例state.location,触发 Router 组件Route组件页面组件 更新。

    react-router-dom 在使用时, 会先使用 React.createContext 构建一个 Context 对象用于页面跳转的 history 对象储存页面 url 信息的 location 对象 等都保存在 Context._currentValue 中。 Router 组件 在渲染过程中,会通过 Context.ProviderContext 对象 引入到 react 应用 中。 Route 组件 在渲染过程中,会通过 Context.Consumer 订阅 Context 的值,然后将 Context._currentValue 通过 props 传递给 页面组件

    页面跳转 时,Router 组件 重新渲染,导致 Context.Provider 组件 重新渲染,然后通知 订阅 Context 的所有 Route 组件 强制更新渲染,重新进行 匹配操作,然后渲染 匹配的页面组件

总结

使用 Context 时要注意的 关键点 如下:

  1. 使用 Context 时需要先通过 React.createContext 构建 Context 对象

  2. 要通过 Context.Provider 将 Context 引入的 react 应用中

  3. 所有要使用 Context 的值的组件必须包裹在 Context.Provider 中

  4. 当 Context.Provider 的 value 值发生变化时,所有使用 Context 的组件会强制更新