前言
用过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;
}
如果user
和password
字段都发生了变化,最后result
为0b11
。
React内部会将这个result
的值和我们在useContext
传入的第二个参数bits.user
进行比较。
// observedBits 为我们使用useContext时传入的第二个参数bits.user
// changedBits 为调用比较函数后返回的值
if((observedBits & changedBits) !== 0){
// 更新该组件
}
如果经过判断,是password
字段发生变化,result
结果为0b10
,我们传入的值bits.user
是0b01
,0b01 & 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
功能,也是实验性质的,所以在控制台有警告。