weixin_39644139
weixin_39644139
2020-12-09 06:33

React源码系列(五): 新 ContextAPI

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

Context API

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

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

React.createContext(defaultValue)

源码地址

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

javascript
// 基本的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是否有被更新

javascript
<reactcontext.provider value="{this.state}">
  <wrap></wrap>
</reactcontext.provider>

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

Class.contextType

javascript
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!

javascript
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为什么好像没测出有多大的区别...

该提问来源于开源项目:jsonz1993/react-source-learn

  • 点赞
  • 回答
  • 收藏
  • 复制链接分享

7条回答

为你推荐

换一换