react-source-learn icon indicating copy to clipboard operation
react-source-learn copied to clipboard

React源码系列(五): 新 ContextAPI

Open jsonz1993 opened this issue 7 years ago • 7 comments

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

jsonz1993 avatar Dec 07 '18 17:12 jsonz1993

Context api组件是不走shouldComponentUpdate生命周期的,这样会不会导致子组件渲染的频率太高了?

mqliutie avatar May 29 '19 02:05 mqliutie

@mqliutie 按错成关闭了.....sorry

现在大一点的react app 基本上都有用redux,这两个东西效果基本是一样的...所以不要太低估js计算能力 而且Context.Provider prop有变动之后,也不是所有子组件都会触发render,所以性能方面我觉得没什么大问题需要考虑

<Provider> // change prop
    <B> // shouldComponentUpdate: return false;  不会触发当前组件render
      <E /> 这个也不会触发render
      <Consumer> // diff render
        <D></D> // diff render
      </Consumer>
    </B>
</Provider>

<A> // change state 
  <B> // shouldComponentUpdate: return false; 所有child都不会触发diff与render
    <C>
      <D></D>
    </C>
  </B>
</A>

jsonz1993 avatar May 29 '19 09:05 jsonz1993

_currentValue和_currentValue2是不是忘记讲了

yhhcg avatar Jul 18 '19 02:07 yhhcg

_currentValue 和 _currentValue2 我也没看到。查了一下,这个主要是为了支持多个 renderer 并发,保证不同渲染器里 value 互不影响。其实他们的作用是一样的,只是分别给主渲染器和副渲染器使用。

As a workaround to support multiple concurrent renderers, we categorize some renderers as primary and others as secondary. We only expect there to be two concurrent renderers at most: React Native (primary) and Fabric (secondary); React DOM (primary) and React ART (secondary). Secondary renderers store their context values on separate fields.

hhking avatar Aug 22 '19 08:08 hhking

把用到 context 组件叫做 consum 👉 把用到 context 组件叫做 consumer(🐶

prprprus avatar Oct 10 '19 09:10 prprprus

@prprprus who?  🙋‍♂️

jsonz1993 avatar Oct 10 '19 09:10 jsonz1993

猩猩~!牛b!!!

prprprus avatar Oct 10 '19 09:10 prprprus