MST
星途 面试题库

面试题:React Context订阅模式下多Provider嵌套问题及解决方案

假设在一个大型React应用中,存在多层Context的Provider嵌套情况。当最内层的组件依赖最外层Provider的数据,但中间层Provider的数据变化也会触发内层组件更新,导致性能问题。请分析出现这种问题的原因,并提出至少两种有效的解决方案,包括对代码结构和逻辑的调整思路。
40.6万 热度难度
前端开发React

知识考点

AI 面试

面试题答案

一键面试

问题原因分析

  1. Context的订阅机制:React的Context采用的是一种“广播”机制,当某个Provider的值发生变化时,所有使用该Context的组件(无论嵌套多深)都会触发重新渲染。中间层Provider数据变化时,由于内层组件也在Context的订阅范围内,所以会被强制重新渲染,即便内层组件仅依赖最外层Provider的数据。
  2. 缺乏细粒度控制:Context没有提供一种简单的方式来精确控制哪些组件应该因特定Provider的变化而更新,这就导致了只要Context值有变化,所有相关组件都会更新,从而引发性能问题。

解决方案

方案一:使用Memo和useCallback

  1. 代码结构调整思路
    • 在内层组件上使用React.memo来包裹,React.memo是一个高阶组件,它会对组件的props进行浅比较,如果props没有变化,组件就不会重新渲染。
    • 对于中间层和最外层Provider中传递给内层组件的函数,使用useCallback进行包裹,确保这些函数在依赖不变的情况下引用保持一致。这样可以避免因为函数引用变化而导致React.memo失效。
  2. 示例代码
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选择器

  1. 代码结构调整思路
    • 创建一个自定义的ContextSelector函数,这个函数接受Context值和一个选择器函数作为参数。选择器函数用于提取组件真正需要的数据。
    • 在组件中使用useContextSelector自定义钩子,它内部使用useContext获取Context值,并通过选择器函数提取数据,同时使用useReduceruseEffect来控制组件更新。只有当选择器函数返回的数据发生变化时,组件才会更新。
  2. 示例代码
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;