Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

Context设计目的是为了共享哪些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

使用示例

首先,要在公共位置定义创建一个Context:
ColorContext.js

  1. // default colors
  2. const colors = {
  3. themeColor: red'
  4. }
  5. export const ColorContext = React.createContext(colors)
  6. // 可以给Context指定展示名称
  7. 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中都可以立即拿到。
42932101-fda1884e-8b73-11e8-969b-afeaa0c49e4e.png

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

react/ReactFiberNewContext.new.js at master · facebook/react · GitHub

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,对应代码在:react-router/HistoryContext.js at master · ReactTraining/react-router · GitHub, react-router/RouterContext.js at master · ReactTraining/react-router · GitHub

如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`]react-router/Router.js at master · ReactTraining/react-router · GitHub中使用对应的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版本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/implementation.ts at master · StringEpsilon/mini-create-react-context · GitHub

核心代码:内部定义了一个EventEmitter,在Provider中value改变时,emit change事件,而在Consumer中则监听value的update事件,从而实现子组件接收Context的值,典型的跨组件通信实现方式,对该方式不提熟悉的同学可以自行了解[EventBus]通信方式,Vue中使用很常见,通过定义一个空的Vue示例作为EventBus,然后组件间通过$emit$on来发布/订阅消息。
vue-component-data-communication-1.png