React学习:context的使用和原理

我们为什么使用context?

  • context提供了一种直接传递数据到组件树深处组件的方式,数据不需要一层一层传递下去。

用法

  1. 改写父组件:添加getChildContext方法和childContextTypes属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class ParentComponent extends React.Component {
    getChildContext() {
    return {color: "purple"};
    }
    render() {
    // ....
    }
    }
    ParentComponent.childContextTypes = {
    color: PropTypes.string
    }
  2. 改写子组件:添加contextTypes属性,并通过this.context访问属性

    • 如果没有定义contextTypesthis.context为空对象

      1
      2
      3
      ChildComponent.contextTypes = {
      color: PropTypes.string
      }
    • 如果定义了contextTypes,子组件的如下生命周期函数将会接收context作为参数

      1
      2
      3
      4
      constructor(props, context)
      componentWillReceiveProps(nextProps, nextContext)
      shouldComponentUpdate(nextProps, nextState, nextContext)
      componentWillUpdate(nextProps, nextState, nextContext)
    • 如果定义了contextTypes,无状态组件也会接收context作为参数

      1
      2
      3
      function ChildComponent (props, context) {
      //...
      }

使用context存在的问题

context + shouldComponentUpdate/PureComponent

  • shouldComponentUpdate的作用:当state没有改变时,控制组件以及其子组件不重新渲染
  • 使用shouldComponentUpdate或者使用PureComponent时,阻止子组件选择的同时也会阻止context的传递

如何安全的使用context

指定一定的规则

  • 规定context是不可变的,并且只在初始化时向下传递一次

使用 injection system

  • 使用一个特殊的数据结构,监听state的变化,强制更新组件。参考这个库:react-broadcast,它使用了这种模式。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    // 数据结构如下
    class Theme {
    constructor(color) {
    this.color = color
    this.subscriptions = [] // listeners
    }
    setColor(color) {
    this.color = color
    this.subscriptions.forEach(f => f()) // 触发listener
    }
    subscribe(f) {
    this.subscriptions.push(f) // 注册listener
    }
    }
    // 在父组件上只要color变化,调用setColor
    class MessageBox extends React.Component {
    constructor (props, context) {
    super (props, context)
    this.theme = new Theme(this.props.color) // 实例化Theme
    }
    // 只要color有更新,就调用setColor
    componentWillReceiveProps(next) {
    this.theme.setColor(next.color)
    }
    getChildContext() {
    return {theme: this.theme};
    }
    render () {
    return <MessageList messages={this.props.messages}/>
    }
    }
    MessageBox.childContextTypes = {
    theme: PropTypes.object
    }
    // 在子组件上注册listener,并通过this.context.theme访问color
    class Button extends React.Component {
    componentDidMount () {
    // 注册listener
    this.context.theme.subscribe(() => this.forceUpdate())
    }
    render() {
    return (
    <button style={{background: this.context.theme.color}}>
    {this.props.children}
    </button>
    );
    }
    }
    Button.contextTypes = {
    theme: PropTypes.object
    };

上述实现过于简单,一个合格的实现需要在componentWillUnmount中清除事件监听器,并且更新state应该使用setState而不是forceUpdate

react如何实现context

ReactFiberContext.js

  • isContextProvider:通过判断父组件是否定义了childContextTypes,来确定父组件是否是一个contextProvider

    1
    2
    3
    export function isContextProvider(fiber: Fiber): boolean {
    return fiber.tag === ClassComponent && fiber.type.childContextTypes != null;
    }
  • isContextConsumer:通过判断组件是否定义了contextTypes,来确定组件是否是一个contextConsumer

    1
    2
    3
    export function isContextConsumer(fiber: Fiber): boolean {
    return fiber.tag === ClassComponent && fiber.type.contextTypes != null;
    }
  • processChildContext:通过父组件获取子组件的context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    export function processChildContext(
    fiber: Fiber,
    parentContext: Object,
    ): Object {
    const instance = fiber.stateNode;
    const childContextTypes = fiber.type.childContextTypes;
    // 声明子组件的context变量
    let childContext;
    startPhaseTimer(fiber, 'getChildContext');
    // 通过getChildContext获取对象赋给childContext
    childContext = instance.getChildContext();
    stopPhaseTimer();
    // 判断getChildContext中定义的对象,是否有在childContextTypes中声明
    for (let contextKey in childContext) {
    invariant(
    contextKey in childContextTypes,
    '%s.getChildContext(): key "%s" is not defined in childContextTypes.',
    getComponentName(fiber) || 'Unknown',
    contextKey,
    );
    }
    return {...parentContext, ...childContext};
    }
  • getMaskedContext:在子组件上获取context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    export function getMaskedContext(
    workInProgress: Fiber,
    unmaskedContext: Object,
    ) {
    const type = workInProgress.type;
    // 子组件必须定义contextTypes,否则会返回一个空对象
    const contextTypes = type.contextTypes;
    if (!contextTypes) {
    return emptyObject;
    }
    // 当context没有变化时,从备份中获取context
    const instance = workInProgress.stateNode;
    if (
    instance &&
    instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext
    ) {
    return instance.__reactInternalMemoizedMaskedChildContext;
    }
    // 创建context
    const context = {};
    for (let key in contextTypes) {
    context[key] = unmaskedContext[key];
    }
    // 备份context
    if (instance) {
    cacheContext(workInProgress, unmaskedContext, context);
    }
    return context;
    }

ReactFiberReconciler.js

  • 更新的时候,子组件会重新获取它的context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    updateContainer(
    element: ReactNodeList,
    container: OpaqueRoot,
    parentComponent: ?React$Component<any, any>,
    callback: ?Function,
    ): void {
    // ...
    // 获取子组件的context
    const context = getContextForSubtree(parentComponent);
    if (container.context === null) {
    container.context = context;
    } else {
    container.pendingContext = context;
    }
    // ...
    }
  • 根据父组件获取子组件的context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function getContextForSubtree(
    parentComponent: ?React$Component<any, any>,
    ): Object {
    // 如果没有父组件,context为emptyObject
    if (!parentComponent) {
    return emptyObject;
    }
    const fiber = ReactInstanceMap.get(parentComponent);
    const parentContext = findCurrentUnmaskedContext(fiber);
    return isContextProvider(fiber)
    ? processChildContext(fiber, parentContext)
    : parentContext;
    }

ReactFiberBeginWork.js

  • 子组件更新context在这个进行

  • 待续。。。