Context
Context 的意思是上下文,许多 React 组件都是通过 Context 来完成的,比如 react-redux 的 <Provider />
<Provider /> 通过 Context 提供一个全局的 store,通过 Context 在组件中分发 Drag 和 Drop 事件
作用
Context 的作用是这样的:
当你不想在组件树中通过逐层传递的方式传递 props 的时候,可以使用 Context 来实现跨越层级的传递
正常情况下我们通过 props 来将属性传递给深层组件
有了 Context,我们不再需要那么麻烦的操作:
使用 Context 一般要用到两个组件:
- Provider(生产者)
- Consumer(消费者)
可以看出来 Context 基于生产消费者模式
在这里,父组件就是生产者,子组件就是消费者。生产者需要通过一个静态属性 childContextTypes 声明提供给自组件的 Context 属性,并且要实现一个 getChildContext 方法,返回代表 Context 的纯对象
假设有一个父组件 parent,包含一个子组件 middle,middle 又包含一个子组件 child,现在要通过 Context 将属性从 parent 传给 child:
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
class Parent extends React.Component {
static childContextTypes = {
a: PropTypes.number,
b: PropTypes.string,
}
getChildContext() {
return {
a: 1,
b: 'string',
}
}
render() {
return <Middle />
}
}
class Middle extends React.Component {
render() {
return <Child />
}
}
父组件内定义一个静态对象 childContextTypes,PropTypes用来规范数据类型,middle 内不用做任何关于传递属性的操作
class Child extends React.Component {
static contextTypes = {
a: PropTypes.number,
}
render() {
const { a, b } = this.context
return (
<div>
a:{a},b:{b}
</div>
)
}
}
ReactDOM.render(<Parent />, document.getElementById('root1'))
子组件内也必须定义一个 contextTypes 静态对象才能够获取 this.context,并且必须要在对象内声明要接受的属性才行,不然值为 undefined。如上,虽然我们定义了 b,但没有在 contextTypes 对象内声明,所以子组件还是拿不到 b 属性的
上述代码执行结果:

可以看到 a 的值 1,而 b 的值则获取不到
对于无状态子组件(单纯含有 render 函数的组件),可以通过如下方式访问父组件的 Context:
const Child = (props, context) => {
const { a } = context
return <div>a:{a}</div>
}
Child.contextTypes = {
a: PropTypes.number,
}
因为 Child 只含有 render 函数,所以可以封装成无状态组件
同样可以得到 a 为 1
发行版本的 Context
在发行版本的 Context 中新增了一个 API:
// 初始化ThemeContext
const ThemeContext = React.createContext({
background: 'red',
color: 'white',
})
通过静态方法 createContext 创建一个 Context,包含两个组件:
- Provider
- Consumer
class App extends React.Component {
render() {
return (
// 向下传递属性,background值改变为green
<ThemeContext.Provider value={{ background: 'green', color: 'white' }}>
<Header />
</ThemeContext.Provider>
)
}
}
class Header extends React.Component {
render() {
return <Title>Hello React Context API</Title>
}
}
class Title extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{(context) => (
// 由于background已经改变,所以子组件拿到的background为green而不是red
<h1 style={{ background: context.background, color: context.color }}>{this.props.children}</h1>
)}
</ThemeContext.Consumer>
)
}
}
Provider 相当于 getChildContext,而 Consumer 子组件必须为一个函数,通过函数的参数获取 Provider 提供的 Context
直接访问父组件 Context 的方法
可以通过构造方法直接访问 Context:
constructor(props, context)
也可以在生命周期中访问:
componentWillReceiveProps(nextProps, nextContext);shouldComponentUpdate(nextProps, nextState, nextContext);componetWillUpdate(nextProps, nextState, nextContext);
在无组件函数中,可以通过函数直接访问组件的 Context:
const StatelessComponent = (props, context) => (
......
)
事实上,React App 就是一个组件树
如图可知,Child 的父组件链为 SubNode -- Node -- App
其实也可以把这个树看成是一颗 Context 树,每个节点的 Context,来自父组件链上所有组件节点通过 getChildContext() 所提供的 Context 对象组合而成的对象
Context 就好比是一个提供给子组件访问的作用域,Context 中的属性可以看作是运行 JS 代码期间可访问的活动对象,包括变量和函数,所以我们可以把 Context 看成一个作用域来使用
但是不建议任何使用都是用 Context
Child 的 Context 来自于 App 和 Node,如果脱离了这两个组件,Child 可能就无法正常工作了,这降低了 Child 的复用性
所以在开发组件的过程中不要随意使用 Context
原因主要有下面几点:
Context目前还处于实验阶段,可能会在后面的发行版本中有大的变化- 对于组件而言,由于影响范围小于
App,如果可以做到高内聚,不破坏组件树的依赖关系,那么还是可以考虑使用Context的 - 对于组件之间的数据通信或者状态管理,优先考虑用
props或者state解决,然后再考虑用其他第三方成熟库解决的,以上方法都不是最佳选择的时候,那么再考虑使用Context Context的更新需要通过setState()触发,但是这并不是可靠的
用 Context 作为共享数据的媒介
App 根节点提供的 Context 数据对象可以看作是 App 全局作用域,所以我们利用根节点组件提供的 Context 对象创建一些 App 级的全局数据,react-redux 就是使用这种方法
如果组件的功能不能单靠组件自身来完成,还需要依赖额外的子组件,那么可以利用 Context 构建一个由多个子组件组合的组件。例如,react-router
react-router 的<Router />自身并不能独立完成路由的操作和管理,因为导航链接和跳转的内容通常是分离的,因此还需要依赖<Link />和<Route />等子组件来一同完成路由的相关工作。为了让相关的子组件一同发挥作用,react-router 的实现方案是利用 Context 在<Router />、<Link />以及<Route />这些相关的组件之间共享一个 router,进而完成路由的统一操作和管理
Context 开发组件
Vue 中有一种组件叫插槽组件 solt,React 对插槽组件的支持不是很好,可以使用 context 开发一套 React 插槽组件
假如我想写一个布局组件,像写成下面这样:
class AppLayout extends React.Component {
static displayName = 'AppLayout'
render() {
return (
<div class="container">
<header>
<Slot name="header"></Slot>
</header>
<main>
<Slot></Slot>
</main>
<footer>
<Slot name="footer"></Slot>
</footer>
</div>
)
}
}
头,尾,和中间分别放一个插槽,在外层使用的时候可以这样:
<AppLayout>
<AddOn slot="header">
<h1>这里可能是一个页面标题</h1>
</AddOn>
<AddOn>
<p>主要内容的一个段落。</p>
<p>另一个段落。</p>
</AddOn>
<AddOn slot="footer">
<p>这里有一些联系信息</p>
</AddOn>
</AppLayout>
实现如上功能需要两个组件:插槽组件 Slot 和 分发组件 AddOn
这两个组件都在 AppLayout 的作用域范围内,所以,可以通过 AppLayout 来连接 Slot 和 AddOn
Slot 负责占位置,提供放置内容的位置,AddOn 负责收集分发内容提供给插槽
功能实现
// SlotProvider.js
function getDisplayName (component) {
return component.displayName || component.name || 'component'
}
export const SlotContext = React.createContext({
requestAddOnRenderer: () => {}
})
const slotProviderHoC = (WrappedComponent) => {
return class extends React.Component {
static displayName = `SlotProvider(${getDisplayName(WrappedComponent)})`
// 用于缓存每个<AddOn />的内容
addOnRenderers = {}
requestAddOnRenderer = (name) => {
if (!this.addOnRenderers[name]) {
return undefined
}
return () => (
this.addOnRenderers[name]
)
}
render () {
const {
children,
...restProps
} = this.props
if (children) {
// 以k-v的方式缓存<AddOn />的内容
const arr = React.Children.toArray(children)
const nameChecked = []
this.addOnRenderers = {}
arr.forEach(item => {
const itemType = item.type
if (item.type.displayName === 'AddOn') {
const slotName = item.props.slot || '$$default'
// 确保内容唯一性
if (nameChecked.findIndex(item => item === stubName) !== -1) {
throw new Error(`Slot(${slotName}) has been occupied`)
}
this.addOnRenderers[stubName] = item.props.children
nameChecked.push(stubName)
}
})
}
return (
<SlotContext.Provider value={
requestAddOnRenderer: this.requestAddOnRenderer
}>
<WrappedComponent {...restProps} />
</SlotContext.Provider>
)
}
}
}
export const SlotProvider = slotProviderHoC
// Slot.js
import { SlotContext } from './SlotProvider.js'
const Slot = ({ name, children }) => {
return (
<SlotContext.Consumer>
{(context) => {
const addOnRenderer = requestAddOnRenderer(name)
return (addOnRenderer && addOnRenderer()) || children || null
}}
</SlotContext.Consumer>
)
}
Slot.displayName = 'Slot'
Slot.propTypes = { name: PropTypes.string }
Slot.defaultProps = { name: '$$default' }