背景
hook中可以使用useContext进行状态管理,具体代码如下:
使用createContext创建一个Context对象
在子组件中使用useContext进行消费
export const AppContext = React.createContext();
父组件中使用createContext创建的Context.Provider。这个组件允许消费组件订阅Context的变化。当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染
// parent.jsx
const [position, setPosition] = useState({
left: 0,
top: 0,
});
const change = () => {
setPosition({
left: position.left++,
top: 0
})
}
return (
<AppContext.Provider value={position}>
<button onClick={change}>change<button>
<Child />
</AppContext.Provider>
)
在子组件中消费Context值的变化
// child.jsx
const store = useContext(AppContext);
return <span>top: {store.top}; random: {Math.random()}</span>
在上面代码中child就能够消费在parent中定义的值。
但是上面这么做存在一个问题,在child组件中我们依赖了store.top的值,但是在父组件更改left值的时候,random值也跟着更改,表示这个组件被多次渲染。这显然是不合理的,那有没有一种办法可以实现这个子组件依赖Context中某个值,当这个值发生改变时组件才重新渲染,其他值发生改变,并不会导致子组件的重新渲染。
大概思路如下:
const store = useStore(['left']);
return <span>child</span>
在上面我们只需要store中left的值,当left值发生改变时,组件才开始重新渲染。store中top的更改不会导致组件的重渲染
使用React.memo
React.memo可用于props的变更检查,当props没有发生改变时,那么该组件就不会渲染,那么Child组件可拆分为两个组件
const InnerChild = React.memo(({ top }) => (
<span>
top: {top}; random: {Math.random()}
</span>
));
function Child() {
const store = useContext(AppContext);
return <InnerChild top={store.top} />;
}
使用React.memo包裹组件,因为父组件只会更改left的值,所以top始终不会发生更改,那么当前组件也就不会重新渲染
使用useMemo进行缓存组件
把创建函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算memoized值。那么就可以把上面的子组件改为以下:
// child
const store = useContext(AppContext);
return useMemo(() => <span>random: {Math.random()}</span>, [store.top]);
这样只有当store.top发生改变时,useMemo返回值才会发生改变。
但是这种方式也有一个弊端,当子组件需要维护大量的状态的时候,useMemo依赖项就需要写很多,就可能导致漏掉而导致状态更新了,DOM树没有更新。
拆分Context
这种思路借鉴于发布订阅者模式,发布者发布数据后,只会对其依赖数据的组件进行更新。
下面是具体的使用方式
const { Provider, useModal } = createModel((initState) => {
const [count, setCount] = React.useState(0);
const [value, setValue] = React.useState('value');
const inc = function () {
setCount(count + 1);
};
const dec = function () {
setCount(count - 1);
};
return { count, inc, dec, value, setValue };
});
然后在父组件中使用Provider进行提供值
function Parent() {
return (
<Provider>
<Child />
<Count />
</Provider>)
}
在Provider中提供两个了两个组件,这两个组件分别依赖了Provider中不同的值,具体如下:
const Child = () => {
const { count, inc, dec } = useModel(['count']);
return (
<div>
{Math.random()}
<Button onClick={dec}>-</Button>
<span>{count}</span>
<Button onClick={inc}>+</Button>
</div>
);
};
const Counter = () => {
const { value, setValue } = useModel(['value']);
return (
<div>
{Math.random()}
<input value={value} onChange={e => setValue(e.target.value)} />
</div>
);
};
在上面代码中Count子组件只希望value值发生改变时,组件重新渲染,而Child组件只期望count值发生改变时,组件重新渲染。
先讲一下这种方式的思路,简单来说就是发布订阅者模式,Provider就是发布者,Child、Count就是订阅者。当Provider值发生改变时,需要通知所以订阅者进行更新。订阅者收到更新通知后,根据对比之前的值判断是否需要更新。
实现发布订阅者模式
首先需要实现一个简单的发布订阅者模式
class Subs {
constructor(state) {
this.state = state;
this.observers = [];
}
add(observer) {
this.observers.push(observer)
}
notify() {
this.observers.forEach(observer => observer())
}
delete(observer) {
const index = this.observers.findIndex(item => item === observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
}
add方法用于往订阅列表中添加订阅者,notify就用通知所有订阅者进行更新
Provider
需要包装Provider,当提供的值发生更改时,需要通知所有的订阅者触发更新
function createModel(model) {
const Context1 = createContext(null);
const Context2 = createContext(null);
const Provider = ({ initState, children }) => {
const containerRef = useRef();
if (!containerRef.current) {
containerRef.current = new Subs(initState);
}
const sealedInitState = useMemo(() => initState, []);
const state = model(sealedInitState);
useEffect(() => {
containerRef.current.notify();
})
return (
<Context1 value={state}>
<Context2 value={containerRef.current}>
{children}
</Context2>
</Context1>
)
}
return {
Provider
}
}
代码还是比较简单的,创建了两个Context,子组件如果需要向useModel(['count'])这么使用那么实际消费的是Context2提供的值,如果直接useModel()就直接消费Context1的值。
useModel
这个函数所需要实现的功能就是在子组件创建的时候把更新函数push到Provider的订阅列表中,具体代码如下:
const useModel = (deps = []) => {
const sealedDeps = useMemo(() => deps, []);
if (sealedDeps.length === 0) {
return useContext(Context1);
}
const container = useContext(Context2);
const [state, setState] = useState(container.state);
const prevDepsRef = useRef([]);
useEffect(() => {
const observer = () => {
const prev = prevDepsRef.current;
const curr = getAttr(container.state, sealedDeps);
if (!isEuqal(prev, curr)) {
setState(container.state)
}
prev.current = curr;
}
container.add(observer)
return () => {
container.delete(observer)
}
}, [])
return state;
}
简单来说在调用的useModel的时候,发布者收集该依赖,然后当值发生更新触发observer函数,这个函数需要比对更改前和更改后的值是否发生更改,如果更改就重新设置state的值。最后返回这个state的值
这样按需加载更新组件就实现了
react-tracked
使用react-tracked也能实现按需更新组件,具体如下代码可参考react-tracked, 大概思路也是参考发布订阅者模式进行按需更新
欢迎各位小伙伴关注我的github,多多点赞ღ( ´・ᴗ・` )比心