Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jsonz1993 opened this issue Dec 7, 2018 · 7 comments
Open

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

jsonz1993 opened this issue Dec 7, 2018 · 7 comments

Comments

@jsonz1993
Copy link
Owner

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

@mqliutie
Copy link

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

@jsonz1993
Copy link
Owner Author

@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 jsonz1993 reopened this Jun 1, 2019
@yhhcg
Copy link

yhhcg commented Jul 18, 2019

_currentValue和_currentValue2是不是忘记讲了

@hhking
Copy link

hhking commented Aug 22, 2019

_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.

@prprprus
Copy link

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

@jsonz1993
Copy link
Owner Author

@prprprus who?  🙋‍♂️

@prprprus
Copy link

猩猩~!牛b!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants