Skip to content

React源码系列(五): 新 ContextAPI  #5

Open
@jsonz1993

Description

@jsonz1993

React16 更新了新的 Context API,在这之前官方一直都不被官方提倡使用。

Context API

截至 Reaact 16.6.3,共提供了四组 api: React.createContextReactContext.ProviderClass.contextTypeReactContext.Consumer

下面我们把提供 context 的组件叫为 provider(提供者),把用到 context 组件叫做 consum(消费者)

React.createContext(defaultValue)

源码地址

该方法传入一个初始值/默认值,创建一个 ReactContext

// 基本的data
const themes = {
  light: {
    color: '#000',
    background: '#eee',
  },

  dark: {
    color: '#fff',
    background: '#222',
  },
};

// 创建 React context 赋默认值
const ReactContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

console.log(ReactContext);

reactcontext

返回的ReactContext包含了我们后面要用到的ConsumerProvider
这里的Consumer其实指向的就是ReactContext,而Provider.context也指向了ReactContext,方便后面值的传递与获取。
细心的同学可能发现这里有两个 value 值,_currentValue_currentValue2,后面会讲到这块。

ReactContext.Provider

Provider 顾名思义既 context 的提供者,我们可以给这个组件传一个value值来覆盖createContext传入的默认值,当value值变化时就会通知到子级的消费者。
所以都是要配合 ReactContext.ConsumerClass.contextType 使用。

一般我们传的时候,不会直接传一个对象。value={{name: 'jsonz'}},因为这样每次render的时候都会认为是全新的 Object。

React内部是根据 Object.is的polyfill 来判断是否value是否有被更新

<ReactContext.Provider value={this.state}>
  <Wrap />
</ReactContext.Provider>

当 provider 的 value 变化时,会把当前 provider 的 value 赋值给 ReactContext._currentValue,后面我们的 consum 可以直接从_currentValue去获取最新的值

Class.contextType

class Child extends React.Component {
  componentDidMount() {
    console.log(this.context);
  }
  render() {
    return <div>{this.context.theme.color}</div>;
  }
  // static contextType = ReactContext
}
Child.contextType = ReactContext;

我们可以通过把 Class.ContextType 指向 ReactContext(也可以用static属性),然后在类的生命周期函数或者 render 函数里面通过 this.context 去获取 ReactContext 值。

但是这种方式有个弊端,就是一个类的 contextType 属性只能指向一个 ReactContext。如果想要同时有多个消费者,就要用到下一小节的 React.Consumer

React 在执行updateClassInstance的时候,会判断该的class有没有contextType这个属性,如果contextType不为空,则返回ReactContext._currentValue,这样我们组件就能拿到最新的 contextValue 了。
当然里面还有很多细节,比如调用生命周期函数ComponentWillReceiveProps之前会加多一个oldContext !== nextContext的判断等等。
有兴趣的可以根据我之前的系列,自己看源码 乐趣更多~

ReactContext.Consumer

ReactContext.Consumer 其实是以组件的形式 consum(消费)ReactContext 的另一种方式。
比起 Class.contextType 最大的不同就是可以同时消费多个 ReactContext,而且他的子级只允许是一个 Function!

const ThemeContext = React.createContext('dark');
const UserContext = React.createContext({ name: 'jsonz' });

function Demo() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <div>
              {user.name} = {theme}
            </div>
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

ReactContext.Consumer在收到需要更新的时候,会去拿组件自身的 currentValue 作为最新的 contextValue,再拿 props.children 当 render 方法,所以我们前面说该组件的子级只能是一个 Function。
此时就算子级返回的是另一个ReactContext.Consumer,那也只是按照刚才的逻辑再走一遍。

综合使用的demo github仓库

demo

新Context API

新的Context API其实依赖 React.CreateContext 生成的组件来维护最新的 currentValue,所以不存在被 shouldComponentUpdate阻断子级 context 更新的问题。

大概的原理是

  1. 当执行workLoop中对fiberTree进行更新时,如果发现ReactContext.Provider组件的值发生更新(变更)的时候,都会去广播。然后找到子级中对应的消费者consum,把他和父级的渲染优先级改为最高优先级(第二步会用到)。

  2. 当执行到某个 classComponent 时,如果这个组件是不需要更新的 (新旧 props、state一致或者shouldComponentUpdate返回了false) ,这时候会去看他子级的 childExpirationTime优先级是否足够高,如果足够高就无视当前的 shouldUpdate,把子级返回到 workLoop里面进行下一次的更新。

这张流程图只是拎了一部分关于context更新的来讲,要了解React整个运行的机制可以看之前的几篇
context

结语

Context API的更新,最直观的进步就是通过组件解决了旧Context被中间组件shouldComponentUpdate阻断的问题,在一定程度上可以代替小部分的Redux使用场景。目前个人的一个小项目就没有引用redux,而是直接在 contentComponet 统一用 ContextAPI 去管理。

至于性能问题 emmm 不知道用 chrome react devtool Profiler为什么好像没测出有多大的区别...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions