Bootstrap

React之Context源码分析与实践

React之Context

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

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

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

使用示例

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

// 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接收属性并传递给消费组件,一个Provider内可以有多个消费组件,并且Provider可以嵌套使用,此时里层的会覆盖外层的数据,多个嵌套时可以参考文档

需要注意的是,当变化时,它内部的所有消费组件都会重新渲染,且Provider及内部消费组件都不受函数影响,而值变化的检测则是使用与相同的方法。可以对Consumer进行缓存,如使用来缓存组件。

当然我们也可以基于上面的代码进行封装,提供一个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版类似,我们可以在子组件内使用来接收Context:

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

React在渲染一个消费组件时,该组件会从组件树中离自身最近的那个匹配的Provider中读取到当前的Context值。

源码分析

先上源码,过滤dev环境代码后,比较少的代码:

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内部属性又指向自身,Provider组件内部value改变时其实会作用到context的_currentValue,而最重要的地方是:

context.Consumer = context

让Consumer直接指向Context本身,则Context值变化,Consumer中都可以立即拿到。

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

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: readContext

就是这么简单的实现~

React-Router之Context使用

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

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

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

中使用对应的Context:

render() {
	return (
		
			
		
	)
}

在高版本的React-Router中,也提供了对应的Hook API ,参考源码,如, 同样是基于上面讲到的HistoryContext和RouterContext,如useHistory:

import React, { useContext } from “react”
import HistoryContext from "./HistoryContext”
export function useHistory() {
	return useContext(HistoryContext)
}

MobX-React之Context使用

MobX-React早期版本提供一对API来方便传递store: /,内部实现就是基于context。

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

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

  • 可以用来选中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内部使用来定义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
    )
	}
}

可见其内部调用了

  • 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依赖的,该库单纯作为对React中方法的polyfill实现,其内部基于Class语法定义了Provider和Consumer两种组件,可以很好地理解内部原理,传送门:

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