返回首页

说说 React 中的 context 的理解?应用场景?

问题解析

面试官考察点:

  • 是否理解 Context 的作用和使用场景
  • 是否掌握 Context API 的使用
  • 是否了解 Context 的性能优化
  • 对 React 数据流设计的理解

核心概念

什么是 Context

Context 提供了一种在组件之间共享数据的方式,而不必显式地通过组件树的逐层传递 props。

Context 主要解决的是 "prop drilling"(属性钻取) 问题,即数据需要经过多层不相关的组件传递才能到达目标组件。

Context 的设计思想

App (提供数据)
├── Toolbar
│   ├── ThemedButton (消费数据)
│   └── UserAvatar (消费数据)
└── Sidebar
    └── UserInfo (消费数据)

在没有 Context 的情况下,Toolbar 和 Sidebar 即使不需要数据,也需要接收并传递 props。

详细解答

1. 基本使用

创建 Context

// ThemeContext.js
import React from 'react';

// 创建 Context,传入默认值
const ThemeContext = React.createContext('light');

export default ThemeContext;

Provider 提供数据

import ThemeContext from './ThemeContext';

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
      <Sidebar />
    </ThemeContext.Provider>
  );
}

Consumer 消费数据

import ThemeContext from './ThemeContext';

// 方式一:使用 Consumer(类组件或函数组件)
function ThemedButton() {
  return (
    <ThemeContext.Consumer>
      {({ theme, setTheme }) => (
        <button
          style={{
            background: theme === 'dark' ? '#333' : '#fff',
            color: theme === 'dark' ? '#fff' : '#333'
          }}
          onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
        >
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

// 方式二:使用 contextType(仅类组件)
class ThemedButtonClass extends React.Component {
  static contextType = ThemeContext;

  render() {
    const { theme, setTheme } = this.context;
    return (
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        Toggle Theme
      </button>
    );
  }
}

// 方式三:使用 useContext(函数组件推荐)
import React, { useContext } from 'react';

function ThemedButtonHook() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      Current theme: {theme}
    </button>
  );
}

2. 多个 Context 的使用

// 创建多个 Context
const ThemeContext = React.createContext('light');
const UserContext = React.createContext({ name: 'Guest' });

function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'John' });

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Layout />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 使用多个 Context
function UserProfile() {
  const { theme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);

  return (
    <div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      <p>User: {user.name}</p>
    </div>
  );
}

// 类组件使用多个 Context
class UserProfileClass extends React.Component {
  render() {
    return (
      <ThemeContext.Consumer>
        {({ theme }) => (
          <UserContext.Consumer>
            {({ user }) => (
              <div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
                <p>User: {user.name}</p>
              </div>
            )}
          </UserContext.Consumer>
        )}
      </ThemeContext.Consumer>
    );
  }
}

3. 动态 Context

// 创建一个完整的主题 Context
const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState(themes.light);

  const toggleTheme = () => {
    setTheme(prevTheme =>
      prevTheme === themes.dark ? themes.light : themes.dark
    );
  };

  const value = useMemo(() => ({
    theme,
    toggleTheme
  }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function App() {
  return (
    <ThemeProvider>
      <Content />
    </ThemeProvider>
  );
}

4. Context 与 useReducer 结合

// 创建 Context
const StateContext = React.createContext();
const DispatchContext = React.createContext();

const initialState = { count: 0, user: null };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'setUser':
      return { ...state, user: action.payload };
    default:
      return state;
  }
}

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

// 自定义 hooks
function useAppState() {
  return useContext(StateContext);
}

function useAppDispatch() {
  return useContext(DispatchContext);
}

// 使用
function Counter() {
  const { count } = useAppState();
  const dispatch = useAppDispatch();

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </div>
  );
}

深入理解

Context 的渲染机制

function App() {
  const [value, setValue] = useState({ count: 0 });

  return (
    <MyContext.Provider value={value}>
      <Child />
    </MyContext.Provider>
  );
}

// 问题:每次渲染都创建新对象,导致所有消费组件都重渲染
// 解决:使用 useMemo
function App() {
  const [count, setCount] = useState(0);

  const value = useMemo(() => ({ count }), [count]);

  return (
    <MyContext.Provider value={value}>
      <Child />
    </MyContext.Provider>
  );
}

Context 的性能优化

// 问题:Context 变化导致所有消费组件重渲染
// 解决:将 Context 拆分

// 拆分为多个 Context
const ThemeContext = React.createContext();
const UserContext = React.createContext();
const LocaleContext = React.createContext();

function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);
  const [locale, setLocale] = useState('en');

  // 每个 Context 独立更新
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const localeValue = useMemo(() => ({ locale, setLocale }), [locale]);

  return (
    <ThemeContext.Provider value={themeValue}>
      <UserContext.Provider value={userValue}>
        <LocaleContext.Provider value={localeValue}>
          <Layout />
        </LocaleContext.Provider>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 组件只订阅需要的 Context
function Header() {
  const { theme } = useContext(ThemeContext);
  const { locale } = useContext(LocaleContext);
  // 不会受 UserContext 变化影响

  return <header style={{ background: theme }}>{locale}</header>;
}

Context 的局限性

// Context 不适合高频更新的场景
function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  // 高频更新会导致所有消费组件频繁重渲染
  return (
    <MouseContext.Provider value={position}>
      <App />
    </MouseContext.Provider>
  );
}

// 解决方案:使用 ref 或状态管理库
function BetterMouseTracker() {
  const positionRef = useRef({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      positionRef.current = { x: e.clientX, y: e.clientY };
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return (
    <MouseContext.Provider value={positionRef}>
      <App />
    </MouseContext.Provider>
  );
}

应用场景

1. 主题切换

const ThemeContext = React.createContext('light');

function ThemedApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
      <MainContent />
      <Footer />
    </ThemeContext.Provider>
  );
}

2. 用户认证

const AuthContext = React.createContext({
  user: null,
  login: () => {},
  logout: () => {}
});

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = async (credentials) => {
    const user = await api.login(credentials);
    setUser(user);
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

3. 多语言支持

const LocaleContext = React.createContext({
  locale: 'en',
  messages: {}
});

function LocaleProvider({ children, locale }) {
  const messages = useMemo(() => loadMessages(locale), [locale]);

  return (
    <LocaleContext.Provider value={{ locale, messages }}>
      {children}
    </LocaleContext.Provider>
  );
}

function useTranslation() {
  const { messages } = useContext(LocaleContext);

  return (key) => messages[key] || key;
}

4. 路由信息

const RouterContext = React.createContext({
  pathname: '/',
  navigate: () => {}
});

function Router({ children }) {
  const [pathname, setPathname] = useState(window.location.pathname);

  const navigate = (path) => {
    window.history.pushState(null, '', path);
    setPathname(path);
  };

  return (
    <RouterContext.Provider value={{ pathname, navigate }}>
      {children}
    </RouterContext.Provider>
  );
}

最佳实践

1. 合理拆分 Context

// 不推荐:一个巨大的 Context
const AppContext = React.createContext({
  theme: {},
  user: {},
  locale: {},
  settings: {}
});

// 推荐:按功能拆分
const ThemeContext = React.createContext();
const UserContext = React.createContext();
const LocaleContext = React.createContext();

2. 封装自定义 Hook

// 封装 useTheme
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// 使用
function Button() {
  const { theme, toggleTheme } = useTheme();
  return <button style={{ background: theme }} onClick={toggleTheme}>Toggle</button>;
}

3. 默认值的使用

const ThemeContext = React.createContext('light');

// 没有 Provider 时,使用默认值
function Button() {
  const theme = useContext(ThemeContext); // 'light'
  return <button style={{ background: theme }}>Click</button>;
}

面试要点

  1. Context 的作用:解决 prop drilling,实现跨层级数据共享
  2. 使用方式:createContext、Provider、Consumer/useContext
  3. 性能注意:Provider value 变化会导致所有消费组件重渲染
  4. 适用场景:主题、用户认证、多语言等全局数据
  5. 不适用场景:高频更新的数据

常见追问:

  • Context 和 Redux 有什么区别?
  • 如何避免 Context 导致的过度渲染?
  • Context 的默认值什么时候生效?
  • 多个 Context 如何嵌套使用?