Categories
程式開發

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 (

)
}

在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 (

{props.children}

)
}

基于Class的ColorProvider如下:

class ColorProvider extends React.Component {
readonly state = { count: 0 };

increment = (delta: number) => this.setState({
count: this.state.count + delta
})

render() {
return (

{props.children}

);
}
}

Class版-使用Consumer

在Provider内部的任务子组件内,都可以使用Context提供的Consumer组件来接收Context内的值:

import { ColorContext } from "./ColorContext”
class Header extends React.Component {
return (

{colors =>

)
}

Hook版-使用Consumer

与Class版类似,我们可以在子组件内使用useContext来接收Context:

import React, { useContext } from “react”
import { ColorContext } from "./ColorContext”
function Header() {
const { colors } = useContext(ColorContext)
return (

)
}

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(
defaultValue: T,
calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext {
if (calculateChangedBits === undefined) {
calculateChangedBits = null;
}

const context: ReactContext = {
$$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中都可以立即拿到。

React之Context源码分析与实践 1

无论是在Class组件或新的Fiber架构中,最终对外提供Context的方法都是readContext:

ReactFiberNewContext.new.js

export function readContext(
context: ReactContext,
observedBits: void | number | boolean,
): T {
let contextItem = {
context: ((context: any): ReactContext),
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: HistoryContext和RouterContext,对应代码在:

HistoryContext.jsRouterContext.js

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

)
}

在高版本的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组件可以传递store或其他内容给子组件,而不需要遍历各层级组件。inject可以用来选中Provider中传递的store,该方法作为一个HOC高阶组件,接收指定的字符串或字符串数组(store名称),并将其传入被包裹的子组件内;或者接收一个函数,其回到参数为全部store,并返回要传递给子组件的stores。

使用示例

定义最外层组件容器,使用Provider传递想要传递的内容

class MessageList extends React.Component {
render() {
const children = this.props.messages.map(message => )
return (

{children}

)
}
}

此处只传递单个属性color,也可以结合mobx定义store,将整个store对象传递下去。

然后在子组件内通过inject选择指定的值:

@inject(“color”)
@observer
class Button extends React.Component {
render() {
return
}
}

class Message extends React.Component {
render() {
return (


{this.props.text}


)
}
}

Provider源码分析

Provider内部使用React.createContext来定义Context

export const MobXProviderContext = React.createContext({})

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 {children}
}

inject源码分析

import { MobXProvider } from "./Provider”
/**
* 可接收一个字符串数组,或一个回调函数:storesToProps(mobxStores, props, context) => newProps
*/
export function inject(...storeNames: Array) {
if (typeof arguments[0] === "function”) {
let grabStoreFn = arguments[0]
return (componentClass: React.ComponentClass) =>
createStoreInjector(grabStoresFn, componentClass, grabStoresFn.name, true)

} else {
return (componentClass: React.ComponentClass) =>
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,
injectNames: string,
makeReactive: boolean
): IReactComponent {
// 支持forward refs
let Injector: IReactComponent = 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来发布/订阅消息。

React之Context源码分析与实践 2