问题原因分析
- Context的订阅机制:React的Context采用的是一种“广播”机制,当某个Provider的值发生变化时,所有使用该Context的组件(无论嵌套多深)都会触发重新渲染。中间层Provider数据变化时,由于内层组件也在Context的订阅范围内,所以会被强制重新渲染,即便内层组件仅依赖最外层Provider的数据。
- 缺乏细粒度控制:Context没有提供一种简单的方式来精确控制哪些组件应该因特定Provider的变化而更新,这就导致了只要Context值有变化,所有相关组件都会更新,从而引发性能问题。
解决方案
方案一:使用Memo和useCallback
- 代码结构调整思路:
- 在内层组件上使用
React.memo
来包裹,React.memo
是一个高阶组件,它会对组件的props进行浅比较,如果props没有变化,组件就不会重新渲染。
- 对于中间层和最外层Provider中传递给内层组件的函数,使用
useCallback
进行包裹,确保这些函数在依赖不变的情况下引用保持一致。这样可以避免因为函数引用变化而导致React.memo
失效。
- 示例代码:
import React, { useCallback, memo } from'react';
// 最外层Context
const OuterContext = React.createContext();
// 中间层Context
const MiddleContext = React.createContext();
const OuterProvider = ({ children }) => {
const outerValue = { data: 'outer data' };
const handleOuter = useCallback(() => {
// 处理外层逻辑
}, []);
return (
<OuterContext.Provider value={{...outerValue, handleOuter }}>
{children}
</OuterContext.Provider>
);
};
const MiddleProvider = ({ children }) => {
const middleValue = { data:'middle data' };
const handleMiddle = useCallback(() => {
// 处理中间层逻辑
}, []);
return (
<MiddleContext.Provider value={{...middleValue, handleMiddle }}>
{children}
</MiddleContext.Provider>
);
};
const InnerComponent = memo(({ outerValue, middleValue }) => {
return (
<div>
<p>Outer Data: {outerValue.data}</p>
<p>Middle Data: {middleValue.data}</p>
</div>
);
});
const App = () => {
return (
<OuterProvider>
<MiddleProvider>
<InnerComponent
outerValue={React.useContext(OuterContext)}
middleValue={React.useContext(MiddleContext)}
/>
</MiddleProvider>
</OuterProvider>
);
};
export default App;
方案二:自定义Context选择器
- 代码结构调整思路:
- 创建一个自定义的
ContextSelector
函数,这个函数接受Context值和一个选择器函数作为参数。选择器函数用于提取组件真正需要的数据。
- 在组件中使用
useContextSelector
自定义钩子,它内部使用useContext
获取Context值,并通过选择器函数提取数据,同时使用useReducer
和useEffect
来控制组件更新。只有当选择器函数返回的数据发生变化时,组件才会更新。
- 示例代码:
import React, { createContext, useContext, useReducer, useEffect } from'react';
// 最外层Context
const OuterContext = createContext();
// 中间层Context
const MiddleContext = createContext();
const createContextSelector = () => {
const subscribers = new Set();
let contextValue;
const notifySubscribers = () => {
subscribers.forEach(subscriber => subscriber());
};
const Provider = ({ value, children }) => {
useEffect(() => {
contextValue = value;
notifySubscribers();
}, [value]);
return <>{children}</>;
};
const useContextSelector = selector => {
const [, forceUpdate] = useReducer(c => c + 1, 0);
useEffect(() => {
const subscriber = () => {
const selectedValue = selector(contextValue);
const previousValueRef = React.useRef(selectedValue);
if (selectedValue!== previousValueRef.current) {
previousValueRef.current = selectedValue;
forceUpdate();
}
};
subscribers.add(subscriber);
subscriber();
return () => subscribers.delete(subscriber);
}, [selector]);
return selector(contextValue);
};
return { Provider, useContextSelector };
};
const { Provider: OuterContextSelectorProvider, useContextSelector: useOuterContextSelector } = createContextSelector();
const { Provider: MiddleContextSelectorProvider, useContextSelector: useMiddleContextSelector } = createContextSelector();
const OuterProvider = ({ children }) => {
const outerValue = { data: 'outer data' };
return (
<OuterContextSelectorProvider value={outerValue}>
{children}
</OuterContextSelectorProvider>
);
};
const MiddleProvider = ({ children }) => {
const middleValue = { data:'middle data' };
return (
<MiddleContextSelectorProvider value={middleValue}>
{children}
</MiddleContextSelectorProvider>
);
};
const InnerComponent = () => {
const outerData = useOuterContextSelector(value => value.data);
const middleData = useMiddleContextSelector(value => value.data);
return (
<div>
<p>Outer Data: {outerData}</p>
<p>Middle Data: {middleData}</p>
</div>
);
};
const App = () => {
return (
<OuterProvider>
<MiddleProvider>
<InnerComponent />
</MiddleProvider>
</OuterProvider>
);
};
export default App;