实现一个简单的React16新版context

3,530 阅读4分钟

最近研究了一些框架原理层的东西,之前用过react16版本新的context api以后感觉比之前的好用很多,同时要想研究redux-react或者mobx-react原理就必须要搞懂新旧context的原理 ,所以经过一点研究,这里实现一个简版的react context。

新版context api文档 官方示例如下👇

const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar(props) {
  return (
    <>
      <ThemedButton />
      <ThemedColor />
    </>
  );
}
// 第一种拿到context方式,仅限类组件
class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}
// 第二种拿到context方式,children的返回值可以是函数组件
function ThemedColor(){
   return (
       <ThemeContext.Consumer>
          {
              value=>(
                   <div>
                      {value}
                   <div/>
              )
          }
       </ThemeContext.Consumer>
   )
}

基本思路

上面是帮大家温习一下用法,下面准备实现思路,核心是属性值的传递可以用类组件的静态属性来做,一个类组件可能有很多实例,但是静态属性不会变的,所有实例都可以取到。

实现provider和consumer组件

通过createContext方法导出一个对象包含两个高阶组件:Provider和Consumer。每次更新时,Provider组都会走静态的getDerivedStateFromProps方法,这时把props中的value挂载给静态属性上,做到提供的值实时变化,而Consumer与Provider共享一块空间,所以每次render都会取Provider上的静态属性value,这样就实现了两个组件的联结。第二种获取context的方式就是让Consumer的children是一个函数,传参给要返回的组件达到更新的目的

function createContext(defaultValue) {
    class Provider extends React.Component {
        static value = defaultValue;
        constructor(props) {
            super(props);
            Provider.value = props.value;
        }
        static getDerivedStateFromProps(nextProps, prevState) {
            Provider.value = nextProps.value;
            return prevState;
        }
        render() {
            return this.props.children;
        }
    }
    class Consumer extends React.Component {
        render() {
            return this.props.children(Provider.value);
        }
    }
    return { Provider, Consumer }
}

静态属性获取context值的方式实现

我们可以观察到上面的实现static contextType = MyContext;, 类把createContext方法生成的Context对象直接放在静态属性上,那么我们每次在render的时候提前把context里面的值赋给this.context 模拟实现如下👇

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    this.context = MyClass.contextType.Provider.value;/* 这一行是模拟实现 */
    let value = this.context;
    return <>
       {value}
    </>
  }
}
    

扩展

以上的只能跑起基本的demo,还有很多问题没有解决

1.provider和consumer之间如果有n个嵌套层级,中间有一级shouldcomponentupdate为false,处于底层的consumer仍然会接到更新,实际源码怎么实现?

实际上,无论在刚才的demo和 老版Context Legacy Context都无法实现这个功能,这也是老版的context为人诟病的其中一点,那新版Context有什么黑魔法可以实现这个呢?

答案是context变化会引发又一次check

下面稍有些晦涩,涉及到一些Fiber的运作机制: 得益于Fiber的链表机制,一个Provider的所有的consumer里面的children都会注册到事件池里面,并提供一个propagateContextChange方法。 Provider变化触发updateContextProvider函数,从而每个consumer的child的Fiber节点会执行propagateContextChange方法(如果位运算通过的话,可以看第二个问题), 在 propagateContextChange 中,以当前 fiber 节点为根的子树中寻找相匹配 Consumer 节点,给与更新标记。

因此,虽然 shouldComponentUpdate 造成了 Consumer的父组件无法被标记更新,但 Provider 的 propagateContextChange 能使 Consumer 组件重新被标记,从而能够被 render。

2.createContext接受的第二个参数用来做什么?

// 源码在此,请注意createContext接收了第二个参数
export function createContext<T>(
    defaultValue: T,
    calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
    if (calculateChangedBits === undefined) {
        calculateChangedBits = null;
    } 
    const context: ReactContext<T> = {
        ?typeof: REACT_CONTEXT_TYPE, // 这个类型标识很特殊,第一个问题与此有关
        _calculateChangedBits: calculateChangedBits,
        _currentValue: defaultValue,
        _currentValue2: defaultValue,
        _threadCount: 0,
        Provider: (null: any),
        Consumer: (null: any),
    };

    context.Provider = {
        ?typeof: REACT_PROVIDER_TYPE,
        _context: context,
    };

    context.Consumer = context;
    
    return context;
}

其实第二个参数是个函数,用来判断比较context是否更新,我们可以用它来自定义更新细粒度从而避免不必要的更新。

这个东西涉及到一些位运算,不展开讲了,后面有时间可能会把这段补充上去。

calculateChangedBits observedBits这两个api是一对,现在这个东西可能不太稳定,慎用

一些参考文章

不一样的 React context

ObservedBits: React Context的秘密功能

React tips — Context API (performance considerations) 需要翻

路漫漫其修远兮,吾将上下而求索,前端工程师永远处在学习的路上