前言
用过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;}
如果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
功能,也是实验性质的,所以在控制台有警告。