漫谈 React 系列(五): 我们来简单聊一下 Context

1,082 阅读6分钟

前言

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

今天我们就借着本文和大家一起聊一聊 Context,了解一下 Context 的日常用法、内部原理以及在常用工具库中的使用。

本文的目录结构如下:

Context 用法及原理解析

Context 的使用,简单来说就是 父组件 提供一个 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 在常用工具库中的使用

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

Context 在 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 对象

Context 在 react-router-dom 中的使用

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

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

在应用 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 的讲解就结束了。最后,我们来总结一下使用 Context 时要注意的 关键点

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

  2. 要通过 Context.ProviderContext 引入到 react 应用中。

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

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