React之Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
Context设计目的是为了共享哪些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
使用示例
首先,要在公共位置定义创建一个Context:
ColorContext.js
// default colors
const colors = {
themeColor: ‘red'
}
export const ColorContext = React.createContext(colors)
// 可以给Context指定展示名称
ColorContext.displayName = "ColorContext”
注意:只有当消费组件所处的组件树中没有匹配的Provider时,default参数才会生效。
在组件树的顶部,使用Provider:
import { ColorContext } from "./ColorContext”
function Root() {
return (
<ColorContext.Provider value={colors}>
<Home/>
</ColorContext.Provider>
)
}
在Provider内的所有组件都可以接收ColorContext,并且Provider接收value
属性并传递给消费组件,一个Provider内可以有多个消费组件,并且Provider可以嵌套使用,此时里层的会覆盖外层的数据,多个嵌套时可以参考文档Context – React。
需要注意的是,当value
变化时,它内部的所有消费组件都会重新渲染,且Provider及内部消费组件都不受shouldComponentUpdate
函数影响,而value
值变化的检测则是使用与Object.is
相同的方法。可以对Consumer进行缓存,如使用React.memo()
来缓存组件。
当然我们也可以基于上面的代码进行封装,提供一个ColorProvider,并提供修改Color的API:
export const ColorProvider = (props) => {
const [color, setColor] = React.useState(colors)
return (
<ColorContext.Provider value={{color, setColor}>
{props.children}
</ColorContext.Provider>
)
}
基于Class的ColorProvider如下:
class ColorProvider extends React.Component {
readonly state = { count: 0 };
increment = (delta: number) => this.setState({
count: this.state.count + delta
})
render() {
return (
<CounterContext.Provider
value={{
count: this.state.count,
updateCount: this.increment,
}}
>
{props.children}
</CounterContext.Provider>
);
}
}
Class版-使用Consumer
在Provider内部的任务子组件内,都可以使用Context提供的Consumer组件来接收Context内的值:
import { ColorContext } from "./ColorContext”
class Header extends React.Component {
return (
<ColorContext.Consumer>
{colors => <ChildComponent style={colors.themeColor}/>
</ColorContext.Consumer>
)
}
Hook版-使用Consumer
与Class版类似,我们可以在子组件内使用useContext
来接收Context:
import React, { useContext } from “react”
import { ColorContext } from "./ColorContext”
function Header() {
const { colors } = useContext(ColorContext)
return (
<ChildComponent style={colors.themeColor}/>
)
}
React在渲染一个消费组件时,该组件会从组件树中离自身最近的那个匹配的Provider中读取到当前的Context值。
源码分析
先上源码,过滤dev环境代码后,比较少的代码:
react/ReactContext.js at 3ca1904b37ad1f527ff5e31b51373caea67478c5 · facebook/react · GitHub
import { REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE } from "shared/ReactSymbols"
import type {ReactContext} from "shared/ReactTypes"
export function createContext<T>(
defaultValue: T,
calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
if (calculateChangedBits === undefined) {
calculateChangedBits = null;
}
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
_calculateChangedBits: calculateChangedBits,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_threadCount: 0,
Provider: (null: any),
Consumer: (null: any),
}
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context
}
context.Consumer = context;
return context
}
创建全局Context的方法非常简单,对外提供Provider、Consumer,其中Provider内部属性_context
又指向自身,Provider组件内部value改变时其实会作用到context的_currentValue,而最重要的地方是:
context.Consumer = context
让Consumer直接指向Context本身,则Context值变化,Consumer中都可以立即拿到。
无论是在Class组件或新的Fiber架构中,最终对外提供Context的方法都是readContext
:
ReactFiberNewContext.new.js
export function readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
let contextItem = {
context: ((context: any): ReactContext<mixed>),
observedBits: resolvedObservedBits,
next: null,
};
if (lastContextDependency === null) {
lastContextDependency = contextItem;
currentlyRenderingFiber.contextDependencies = {
first: contextItem,
expirationTime: NoWork,
};
} else {
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
看下useContext
的实现:
就是这么简单的实现~
React-Router之Context使用
这部分主要是通过解读React-Router源码中对Context的使用,来加深对其的了解。
React-Router项目中主要定义了两个Context: HistoryContext
和RouterContext
,对应代码在:
如RouterContext源码:
// mini-create-react-context,类createContext API, 计划替换中
import createContext from "mini-create-react-context”
const createNamedContext = name => {
const context = createContext();
context.displayName = name;
return contex
}
const context = createNamedContext(“Router”)
export default context;
Router.js中使用对应的Context:
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
<HistoryContext.Provider children={this.props.children || null} value={this.props.history}/>
</RouterContext.Provider>
)
}
在高版本的React-Router中,也提供了对应的Hook API ,参考源码react-router/hooks.js at master · ReactTraining/react-router · GitHub,如useLocation
, useHisotry
同样是基于上面讲到的HistoryContext和RouterContext,如useHistory:
import React, { useContext } from “react”
import HistoryContext from "./HistoryContext”
export function useHistory() {
return useContext(HistoryContext)
}
MobX-React之Context使用
GitHub - mobxjs/mobx-react: React bindings for MobX MobX-React早期版本提供一对API来方便传递store: Provider
/inject
,内部实现就是基于context。
注意,通常在新的代码实现中已经不在需要使用Provider
和inject
,其大部分功能已经被React.createContext
覆盖
使用示例
定义最外层组件容器,使用Provider传递想要传递的内容
class MessageList extends React.Component {
render() {
const children = this.props.messages.map(message => <Message text={message.text} />)
return (
<Provider color=“red”>
<div>{children}</div>
</Provider>
)
}
}
此处只传递单个属性color,也可以结合mobx定义store,将整个store对象传递下去。
然后在子组件内通过inject选择指定的值:
@inject(“color”)
@observer
class Button extends React.Component {
render() {
return <button style={{ background: this.props.color }}>{this.props.children}</button>
}
}
class Message extends React.Component {
render() {
return (
<div>
{this.props.text} <Button>Delete</Button>
</div>
)
}
}
Provider源码分析
Provider内部使用React.createContext
来定义Context
export const MobXProviderContext = React.createContext<IValueMap>({})
export interface ProviderProps extends IValueMap {
children: React.ReactNode
}
export function Provider(props: ProviderProps) {
const { children, ...stores } = props
// 通过useContext消费Context
const parentValue = React.useContext(MobXProviderContext)
// 通过ref保持所有context值
const mutableProviderRef = React.useRef({ …parentValue, …stores })
const value = mutableProviderRef.current
return <MobXProviderContext.Provider value={value}>{children}</MobXProviderContext.Provider>
}
inject源码分析
import { MobXProvider } from "./Provider”
/**
* 可接收一个字符串数组,或一个回调函数:storesToProps(mobxStores, props, context) => newProps
*/
export function inject(...storeNames: Array<any>) {
if (typeof arguments[0] === "function”) {
let grabStoreFn = arguments[0]
return (componentClass: React.ComponentClass<any, any>) =>
createStoreInjector(grabStoresFn, componentClass, grabStoresFn.name, true)
} else {
return (componentClass: React.ComponentClass<any, any>) =>
createStoreInjector(
grabStoresByName(storeNames),
componentClass,
storeNames.join(“-“),
false
)
}
}
可见其内部调用了createStoreInjector(grabStoreFn, componentClass, storesName, boolean)
function createStoreInjector(
grabStoresFn: IStoresToProps,
component: IReactComponent<any>,
injectNames: string,
makeReactive: boolean
): IReactComponent<any> {
// 支持forward refs
let Injector: IReactComponent<any> = React.forwardRef((props, ref) => {
const newProps = { …props }
// 通过useContext来消费全局的Context
const context = React.useContext(MobXProviderContext)
// 赋值操作,将指定store作为子组件的最新props
Object.assign(newProps, grabStoresFn(context || {}, newProps) || {})
if (ref) {
newProps.ref = ref
}
// 返回包裹后的子组件
return React.createElement(component, newProps)
})
// inject接收函数回调时,则默认讲组件变为observer
if (makeReactive) Injector = observer(Injector)
Injector[“isMobxInjector"] = true // assigned late to suppress observer warning
// 拷贝子组件的静态方法
copyStaticProperties(component, Injector)
// 将wrappedComponent指向原始子组件
Injector[“wrappedComponent”] = component
Injector.displayName = getInjectName(component, injectNames)
return Injector
}
总结
上面关于React的Context内容已经结束了,包括基本使用方式,又通过源码解读来深入了解其原理,最后学习React-Router和MobX-React库的源码彻底掌握Context的使用场景。
不想结束的部分
Provider与Consumer本身,作为React中的特殊组件类型,有其特殊的实现方式,本文并没有仔细去分析。如果想深入了解其实现原理,可以自行去阅读React源码,但是直接阅读React代码库是比较费力的,分析定位起来会比较复杂。
给爱学习的同学推荐React-Router依赖的mini-create-react-context
,该库单纯作为对React中createContext
方法的polyfill实现,其内部基于Class语法定义了Provider和Consumer两种组件,可以很好地理解内部原理,传送门:mini-create-react-context
核心代码:内部定义了一个EventEmitter,在Provider中value改变时,emit change事件,而在Consumer中则监听value的update事件,从而实现子组件接收Context的值,典型的跨组件通信实现方式,对该方式不提熟悉的同学可以自行了解[EventBus]通信方式,Vue中使用很常见,通过定义一个空的Vue示例作为EventBus,然后组件间通过$emit
和$on
来发布/订阅消息。
评论