ObservedBits: React Context的秘密功能

ObservedBits: React Context的秘密功能

上回讲到使用React Context要防止重复渲染,但是,即使我们做到避免Context.Provider重复渲染子组件(不知道我在说什么的请先看上回文章),依然避免不了多个Context.Consumer会因为和自己不相关的内容改变而重复渲染。

举个例子,有一个Context Provider有两个Consumer,从来没有人规定说一个Provider只能对应一个Consumer啊,然后context上的数据差不多是这样。

{
  theme: /* 关于主题 */
  count: /* 给最佳hello world奖得住 counter */
}

下面是一个组件的关系图,很明显,两个Consumer依赖的数据虽然都放在一个Context里,但是确实完全不同的两个部分。

从逻辑上说,一个Consumer A(我们叫它Content)只依赖于theme字段,另一个Consumer B(我们叫它Counter)只依赖于count字段,所以,理想状况是,只有theme更改的时候,Content才重新渲染,只有count更改的时候,Counter才需要重新渲染。

可惜,做不到,因为React中Context的逻辑是,只要Provider的value一变,所有的Consumer都会得到通知,才不管你只依赖于value中某个字段呢。

在传统Context API中,Content组件就是这样写。

const Content = () => (
  <Context.Consumer>
     {
        contextValue => {
          // 其实这里只有contextValue.theme改变的时候才需要被调用啊
          // 但是,对不起,只要contextValue改了,这里就要被调用
        }
     }
  </Context.Consumer>
);

如果使用Hooks,那就是这样。

const Content = () => (
   const {theme} = useContext(Context);

   // 你看,我真的只需要theme,但是只要Context的value一改,这个Content总是被重新渲染。
);

现在你应该明白这个窘境了,简单来说就是这样,Context的Consumer中需要一个途径告诉React:"我不需要更新,用我上一次渲染的结果就行。"

以前我以为React没有这种途径,最近一个偶然的机会,我发现其实React还是提供了类似功能的,不过这个功能非常秘密,叫做observedBits。

先给FBI警告⚠️,这个功能处于unstable状态,仅供参考,如果要用,后果自负。

你要知道为啥这个功能是一个“秘密功能”,因为React也拿不准这个功能好不好,所以拿出来半遮半掩让人试试,如果大家觉得不好,会默默地删掉。

好,我们来看这个功能。

React利用createContext函数创建一个"上下文"(Context),第一个参数是默认值,其实就连第一个参数一般都不怎么用,所以第二个可选参数就更加被人忽略了,第二个参数可以是一个函数,每当Context的值发生变化时,这个函数被用来调用。

const Context = createContext(null, (prev, next) => {
  //prev是上一次context的值
  //next是新的context值
});

React预期这个函数参数返回一个数字,以bit的方式代表值的哪些部分发生了变化,我希望你理解什么叫"比特操作",简单说就是……算了,你要是忘了就去翻计算机科学课程吧。

就接着上面Content和Counter两个Consumer的例子说,现在Content只关心theme字段的辩护啊,Counter只关心count字段的变化,所以,我们用一位bit来代表theme的变化,另一个bit位来代表theme的变化。

别写magic number,用常数定义下来。

const ThemeColorChangedBits = 0b10;
const ThemeCountChangedBits = 0b01;

比较prev和next,如果只有theme变化,就返回0b10;如果只有count变化,就返回0b01;如果theme和count都变化,就返回0b11;如果theme和count都没变化……都没变化这个函数根本不会被调用啊!

Context就像下面这样来创建。

const Context = createContext(null, (prev, next) => {
  let result = 0;
  if (prev.theme !== next.theme) {
    result |= ThemeColorChangedBits;
  }
  if (prev.count !== next.count) {
    result |= ThemeCountChangedBits;
  }
  console.log("calculatedBits ", result);
  return result;
});

上面说的是创建Context,我们赋予了这个Context一个神力,就是通过createContext的第二个函数参数判断改了哪些东西,用bits代表。

接下来看怎么用这些bits。

以只关心theme字段的Content组件为例,代码这么写。

function Content() {
  return (
    <Context.Consumer unstabled_observedBits={ThemeColorChangedBits}>
    {
      ({theme, switchTheme}) => {
        /* 只有theme改变才调用到这里 */
        return (
          <>
            <h1 style={theme}>Hello world</h1>
            <button onClick={() => switchTheme(redTheme)}>Red Theme</button>
            <button onClick={() => switchTheme(greenTheme)}>Green Theme</button>
          </>
        );
      }
    }
    </Context.Consumer>
  );
}

这样一来,原本每次Context值变化都会被调用的Consumer子组件部分,变成只有theme字段改变才调用了。

上面用的是传统Context API,通过prop名unstable_observedBits就看得出来是一个不稳定API,不过,如果你用上Hooks,就看不出来这是一个unstable的东西了,代码如下。

function Content() {
  console.log("render Content");
  const { theme, switchTheme } = useContext(Context, ThemeColorChangedBits);

  return (
    <>
      <h1 style={theme}>Hello world</h1>
      <button onClick={() => switchTheme(redTheme)}>Red Theme</button>
      <button onClick={() => switchTheme(greenTheme)}>Green Theme</button>
    </>
  );
}

这个工作过程是怎样的呢?

每次当Context的值发生变化的时候,React会去调用createContext的第二个参数,返回结果不是一个bits嘛,用这个bits去和每个Context Consumer给的 unstabled_observedBits 做按位AND。

比如,当只有theme改变的时候,得到的bits是0b10,这个bits和Content给的unstabled_observedBits(值是0b10)做按位AND,得到的是0b10,结果不是0,所以知道需要重新渲染Content。

0b10 & 0b10 === 0b10 //需要重新渲染

如果和Counter给的unstabled_observedBits (值是0b01)做按位AND,得到的是 0,所以就不需要重新渲染Content。

0b10 & 0b01 === 0b00 //不需要重新渲染

假设,某一次更新既更新了theme,也更新了count,那么得到的bits就是0b11,那么,和Content还有Counter的unstabled_observedBits按位AND结果都不是0,所以Content和Counter都会重新渲染。

完整demo代码在这里可以看到 observedBits - CodeSandbox

大家通过Console上打出的内容可以判断,当切换主题时,Counter真的没有重新渲染;当点+按钮时,Content也真的没有重新渲染。

总结

这个秘密API怎么样呢?

我个人觉得不怎么样,以为动用了bit操作,bit操作本身就很烦,让应用层API去搞bit操作,总是感觉怪怪的。

而且,一个数字的bit位是有限的,也就是说能够代表context修改的部分数量也是有限的,没法扩展出很多可能独立修改的部分。

目前observedBits这招还是unstable,我觉得将来肯定会废弃掉,至于React会提供什么样的API实现同样的目的,我们拭目以待吧。

最后做个广告:加入我的知识星球《进击的React》,获取最新React技术咨询,加入后分享邀请好友加入还可以获得奖金哦

编辑于 2018-11-30

文章被以下专栏收录