写点什么

React 之 Context 源码分析与实践

用户头像
费马
关注
发布于: 2020 年 06 月 03 日
React之Context源码分析与实践

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) {
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
currentlyRenderingFiber.contextDependencies = {
first: contextItem,
expirationTime: NoWork,
};
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}



看下useContext的实现:

useContext: readContext

就是这么简单的实现~

React-Router之Context使用

这部分主要是通过解读React-Router源码中对Context的使用,来加深对其的了解。



React-Router项目中主要定义了两个Context: HistoryContextRouterContext,对应代码在:

如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。



注意,通常在新的代码实现中已经不在需要使用Providerinject,其大部分功能已经被React.createContext覆盖



  • Provider组件可以传递store或其他内容给子组件,而不需要遍历各层级组件。

  • inject可以用来选中Provider中传递的store,该方法作为一个HOC高阶组件,接收指定的字符串或字符串数组(store名称),并将其传入被包裹的子组件内;或者接收一个函数,其回到参数为全部store,并返回要传递给子组件的stores。

使用示例

定义最外层组件容器,使用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)

  • grabStoreFn: 用来处理选择哪些store,当参数为函数时则使用自定义函数作为处理函数

  • componentClass: 子组件

  • storesName: 需要选择的store名称

  • boolean: 是否将组件监听变为observer



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来发布/订阅消息。



发布于: 2020 年 06 月 03 日阅读数: 1143
用户头像

费马

关注

一个想当厨子的程序员@杭州 2018.07.28 加入

公众号:【也寻常】 在杭州的同学,来加个微信吧

评论

发布
暂无评论
React之Context源码分析与实践