背景
目前我们的应用很多是采用 createContainer 来管理状态的,实现逻辑和 unstated-next 类似,但是它依然存在 Context 状态变化时会渲染所有消费者组件的问题。
有一种通过双 Context 实现依赖收集,按需渲染的设计,一起来学习一下。
状态管理
首先来实现一个状态管理的功能,类似于 unstated-next。本质上是 Context 和 Hooks 组合的模式,其中 useHook 是用户自定义的 hooks,用来生产数据;Provider 用来提供数据;useContainer 用来消费数据。
function createContainer(useHook) {
const Context = createContext(null);
// 用来提供数据
const Provider = function Provider({ children }) {
const state = useHook();
return (
<Context.Provider value={state}>
{children}
</Context.Provider>
);
};
// 消费数据
const useContainer = function () {
const value = useContext(Context);
return value;
};
return {
Provider,
useContainer,
};
}
用一个示例来演示如何使用 createContainer:
const { Provider, useContainer } = createContainer(function useHook() {
const [counter, setCounter] = useState(0);
return {
counter,
setCounter
};
});
function Child() {
const { counter, setCounter } = useContainer();
return <div onClick={() => setCounter(counter + 1)}>{counter}</div>
}
function App() {
return <Provider>
<Child />
</Provider>
}
依赖收集
通过 Context 管理状态,如果 Context 状态变化,每个消费 Context 状态的组件都会重新渲染,但是我们希望如果组件只用到了数据中的某个字段,只有当这个字段变化时才重新渲染。
举个例子:Context 的状态是 { a: 1, b: 1 },组件 A 用到了字段 a,我们期望的是只有当 a 变化时组件 A 才重新渲染,b 变化时不重新渲染。
如果想实现上面的功能,就需要收集依赖,当数据变化时,定向触发依赖的执行。
既然 Context 状态变化会引起消费者渲染,那么消费者就不能依赖有状态 Context,所以我们加一层无状态 Context 来提供数据,据此来调整一下 createContainer:
function createContainer(useHook) {
// 有状态
const IntrinsicContext = createContext(null);
// 无状态,下面会通过 ref 来保存最新的数据
const ObservableContext = createContext(null);
const Provider = function Provider({ children }) {
// ....
return (
<IntrinsicContext.Provider value={state}>
<ObservableContext.Provider value={observableValue}>
{children}
</ObservableContext.Provider>
</IntrinsicContext.Provider>
);
};
为了在 IntrinsicContext 状态变化时,通知组件渲染,我们引入观察者模式,其中 state 保存最新数据,observers 保存消费数据的回调函数:
function createContainer(useHook) {
// 有状态
const IntrinsicContext = createContext(null);
// 无状态,通过 ref 来保存最新的数据
const ObservableContext = createContext(null);
const Provider = function Provider({ children }) {
// 通过 ref 来保存数据,不会触发组件渲染
const { current: observableValue } = useRef({ state: null, observers: [] });
const state = useHook();
// 保存最新的数据
observableValue.state = state;
useEffect(() => {
// 通知观察者
observableValue.observers.forEach(observer => observer());
});
return (
<IntrinsicContext.Provider value={state}>
<ObservableContext.Provider value={observableValue}>
{children}
</ObservableContext.Provider>
</IntrinsicContext.Provider>
);
};
// ....
const useContainer = function (_depCb) {
// ...
}
}
上面实现了 Provider 可以提供数据了,下面来看如何消费数据,消费数据的两个重点是:1、需要知道哪些字段(依赖)变化时,需要触发渲染;2、字段(依赖)变化时如何处理,也就是 observer:
function createContainer(useHook) {
// ....
// depCb 用来返回依赖列表,比如 () => [field1, field2]
const useContainer = function (_depCb) {
const depCp = useCallback(_depCb, []);
const observableValue = useContext(ObservableContext);
const [state, setState] = useState(observableValue.state);
const prevDepsRef = useRef([]);
useEffect(() => {
const observer = () => {
const prev = prevDepsRef.current;
const cur = depCp(observableValue.state);
// 通过浅比较,来判断依赖是否有变化
if (!shallowCompare(prev, cur)) {
// 触发渲染
setState(observableValue.state);
}
prevDepsRef.current = cur;
}
observableValue.observers.add(observer);
return () => {
observableValue.observers.delete(observer);
};
}, []);
return state;
};
}
代码优化
为了让代码更具可读性和可维护性,可以把 { observers: [], state: null } 封装成 ObservableValue,比如:
class ObservableValue {
constructor(state) {
this.observers = new Set();
this.state = state;
}
notify() {
for (const observer of this.observers) {
observer();
}
}
}
总结
本文通过 Context + Hooks 实现了对开发者友好的状态管理工具,然后我们又通过双 Context 的模式实现了 React 的依赖收集,两种实现都很巧妙。
如果对你有启发,欢迎点赞、评论。