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 结果为 0b010 & 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功能,也是实验性质的,所以在控制台有警告。