前言
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子) 进行传递的。但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。针对这种情形,Context 提供了一种在组件之间 共享 此类值的方式,而不必显式地通过组件树的逐层传递 props。
今天我们就借着本文和大家一起聊一聊 Context,了解一下 Context 的日常用法、内部原理以及在常用工具库中的使用。
本文的目录结构如下:
Context 用法及原理解析
Context 的使用,简单来说就是 父组件 提供一个 Context 对象 供 子组件(及子组件的子组件等) 使用。当 Context 对象 的值发生变化时,使用 Context 对象 的 子组件(及子组件的子组件等) 触发 更新,重新渲染。
使用 Context
Context 的使用分为 三步:
-
构建 Context 对象;
要在 react 应用 中使用 Context,我们需要先使用 react 提供的 createContext 方法构建一个 Context 对象, 如下:
const Const = React.createContext();
Context 对象 中有三个 关键属性,他们依次为 _currentValue、Provider、Consumer。
Context._currentValue, 对应的值为 供子组件使用的 context,默认为 undefined。 在使用 createContext 时,我们可以传入一个 初始值,_currentValue 会使用传入的初始值初始化。
在子组件中使用的 context 值,都是从 Context._currentValue 获取的。
Context.Provider,react 组件,用于更新 Context._currentValue 并通知使用 Context 对象 的 子组件 更新。
Context.Consumer,react 组件,用于将 Context._currentValue 传递给子组件并订阅 Context._currentValue 的变化。
订阅, 即 Context._currentValue 发生变化,使用 Context 的子组件都需要更新,重新渲染。
-
父组件将 Context 对象引入 react 应用供子组件使用;
Context 对象 构建完成以后,接下来要做的就是将 Context 对象 引入 react 应用供子组件订阅。
通过 Context.Provider, 我们可以将 Context 对象 引入 react 应用,如下:
<Context.Provider value={value}> <Child /> </Context.Provider>
Provider 接收一个 value 属性, value 属性的值会用于更新 Context._currentValue。 当 value 的值发生变化时,会通知所有使用 Context 的子组件更新。
-
子组件获取 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 方式 局限性比较大,缺点如下:
-
仅适用于类组件;
-
每次只能使用一个 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 对象。
-
原理解析
当我们通过 contextType、useContext、Consumer 方式为子组件获取 Context._currentValue 值时,会 订阅 Context 对象。
在组件渲染过程中, react 会为每一个组件构建一个对应的 虚拟节点 - fiber node 对象。如果子组件通过 contextType、useContext、Consumer 方式获取 Context._currentValue, 那么对应的 Context 对象 就会保存到 fiber node 对象的 dependencies 列表中。
当父组件修改 Context.Provider 的 value 属性的值时,会触发 Context.Provier 组件的更新。 在更新过程中,会依次遍历所有子组件虚拟节点的 dependencies 列表,查看 dependencies 中是否包含当前 Context 对象。如果有,强制子组件更新,重新渲染,获取最新 Context._currentValue。
注意,上述三种方式,都需要被 Context.Provier 包裹,否则无效。
Context 在常用工具库中的使用
Context 在实际的 react 应用 中是有大量使用的,最常见的就是 react-redux 和 react-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.Provider 将 Context 对象 引入 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.Provider 将 Context 对象引入到 react 应用中。 Route 组件在渲染过程中,会通过 Context.Consumer 订阅 Context 的值,然后将 Context._currentValue 通过 props 传递给页面组件。
当页面跳转时,Router 组件重新渲染,导致 Context.Provider 组件重新渲染,然后通知订阅 Context 的所有 Route 组件强制更新渲染,重新进行匹配操作,然后渲染匹配的页面组件。
写在最后
到这里,关于 Context 的讲解就结束了。最后,我们来总结一下使用 Context 时要注意的 关键点:
-
使用 Context 时需要先通过 React.createContext 构建 Context 对象。
-
要通过 Context.Provider 将 Context 引入到 react 应用中。
-
所有要使用 Context 的值的组件必须包裹在 Context.Provider 中。
-
当 Context.Provider 的 value 值发生变化时,所有使用 Context 的组件会强制更新。