ZHANGYU.dev

October 14, 2023

React Context精准更新

React3.0 min to read

前言

用过Context的同学都知道,Context是无法像Redux一样精准更新的,当Context中某一个值改变,所有使用了该Context的组件一定都会更新,用Context来做状态管理,一定会有一些小小的性能损失。

但是在我阅读Context源码的过程中,发现了一个文档上没有描述的方法,createContext方法和useContext方法都有第二个参数,可以做到精准的更新使用了Context中某一项值的组件,而不会导致所有组件全部刷新。

实现原理

原理很有React的风格,使用位运算

按位或( | ) a | b	
对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。
按位与( & ) a & b
对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。

0 | 0b01 结果为 0b01
0 & 0b01 结果为 0

我们需要对字段增加一个二进制的标识,用来标识值是否有改变。

const bits = {
  user: 0b01,
  password: 0b10,
}

我们的组件在使用Context的时候,需要标识该组件使用了那些字段。

const context = useContext(Context, bits.user); // 这个context使用了user字段

Context的值改变的时候,会运行一个比较函数。

const calculateChangedBits = (oldValue, newValue) => {
  let result = 0;
  // 标识user字段发生变化
  if (oldValue.user !== newValue.user){
    result |= bits.user; // 0 -> 0b01
  }
  // 标识password字段发生变化
  if (oldValue.password !== newValue.password){
    result |= bits.password; // 0 -> 0b10
  }
  return result;
}

如果userpassword字段都发生了变化,最后result0b11

React内部会将这个result的值和我们在useContext传入的第二个参数bits.user进行比较。

// observedBits 为我们使用useContext时传入的第二个参数bits.user
// changedBits 为调用比较函数后返回的值
if((observedBits & changedBits) !== 0){
  // 更新该组件
}

如果经过判断,是password字段发生变化,result结果为0b10,我们传入的值bits.user0b010b01 & 0b10 结果为0,所以不会更新组件。

一个例子

可以在codesandbox查看。

import { createContext, useReducer, useContext, useRef } from "react";

// 计算是那些值发生了变化
const calculateChangedBits = (oldValue, newValue) => {
  let result = 0;
  Object.entries(newValue.state).forEach(([key, value]) => {
    if (value !== oldValue.state[key]) {
      result |= bitsMap[key];
    }
  });
  return result;
};

// 字段的标识
const bitsMap = {
  user: 0b01,
  password: 0b10
};

const initialValue = { user: "", password: "" };
const reducer = (state, { name, value }) => ({ ...state, [name]: value });

// 第二个参数为比较函数
const Context = createContext(initialValue, calculateChangedBits);

const Input = ({ name }) => {
  // useContext第二个参数标识该组件使用了哪一个字段
  const { state, setState } = useContext(Context, bitsMap[name]);
  const renderCount = useRef(0);
  ++renderCount.current;
  return (
    <div>
      <input
        type="text"
        value={state[name]}
        onChange={({ target }) => setState({ name, value: target.value })}
      />
      <p>渲染了{renderCount.current}次</p>
    </div>
  );
};

const Provider = ({ children }) => {
  const [state, setState] = useReducer(reducer, initialValue);
  return (
    <Context.Provider value={{ state, setState }}>{children}</Context.Provider>
  );
};

export default function App() {
  return (
    <Provider>
      <Input name="user" />
      <Input name="password" />
    </Provider>
  );
}

useContext的第二个参数除了二进制标识,也可以传false,表示不需要更新,这在传递dispatch方法的时候会很有用。

const useDispatch = () => {
  const { dispatch } = useContext(Context, false);
  return dispatch;
}

ContextConsumer中使用。

<Context.Consumer unstable_observedBits={bitsMap[name]}>
  {({ state })=>{...}}
</Context.Consumer>

需要注意的地方

由于是二进制的值,所以有一个最大值的限制,如在32位系统上最大的二进制值为0b111111111111111111111111111111,也就是只能记录31个字段的标识。

由于是future功能,也是实验性质的,所以在控制台有警告。