MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 中 Context 的工作原理

2021-04-261.5k 阅读

React 中的 Context 简介

在 React 应用中,数据通常通过 props 自上而下(parent to child)传递,但在某些场景下,这样的传递方式显得繁琐,例如某些数据需要在多个组件层级间共享,而这些组件并非直接的父子关系。Context 提供了一种在组件树中共享数据的方式,使得我们可以不必通过层层传递 props 来实现数据共享。

创建 Context

在 React 中,使用 createContext 函数来创建一个 Context 对象。该函数接受一个默认值作为参数,这个默认值会在消费组件(consumer components)在没有匹配到 Provider 时使用。以下是创建 Context 的基本代码示例:

import React from 'react';

// 创建一个 Context
const MyContext = React.createContext('default value');

export default MyContext;

这里创建了一个名为 MyContext 的 Context,默认值为 default value

Context.Provider

Context.Provider 是一个 React 组件,它接收一个 value 属性,这个属性的值会被传递给消费该 Context 的所有后代组件。它允许我们在组件树的某个层级上提供数据,供下面的组件使用,而无需通过中间组件层层传递。示例如下:

import React from 'react';
import MyContext from './MyContext';

const App = () => {
  const sharedValue = 'actual value';
  return (
    <MyContext.Provider value={sharedValue}>
      {/* 子组件树 */}
    </MyContext.Provider>
  );
};

export default App;

在上述代码中,App 组件通过 MyContext.Provider 提供了 sharedValue,这个值可以被其后代组件访问。

消费 Context

使用 Context.Consumer

一种消费 Context 的方式是使用 Context.Consumer。它是一个 React 组件,接受一个函数作为子元素(function as a child)。该函数接收当前 Context 的值作为参数,并返回一个 React 节点。示例如下:

import React from'react';
import MyContext from './MyContext';

const ChildComponent = () => {
  return (
    <MyContext.Consumer>
      {value => (
        <div>
          The value from context is: {value}
        </div>
      )}
    </MyContext.Consumer>
  );
};

export default ChildComponent;

ChildComponent 中,通过 MyContext.Consumer 接收 Context 的值,并在组件中展示出来。

使用 useContext Hook

从 React 16.8 版本开始,引入了 Hook。useContext Hook 提供了一种更简洁的方式来消费 Context。示例代码如下:

import React, { useContext } from'react';
import MyContext from './MyContext';

const AnotherChildComponent = () => {
  const value = useContext(MyContext);
  return (
    <div>
      Value from context using useContext: {value}
    </div>
  );
};

export default AnotherChildComponent;

AnotherChildComponent 中,通过 useContext(MyContext) 获取 Context 的值,相比于 Context.Consumer,这种方式更加简洁直观,尤其是在函数组件中。

Context 的工作原理深入剖析

React 中的组件树与 Context 传递路径

在 React 应用中,组件构成了一棵树状结构。Context 的传递是基于这棵组件树的。当一个组件通过 Context.Provider 提供了 Context 值时,React 会在组件树中建立一条特殊的 “数据传递路径”。这条路径从 Provider 开始,向下延伸到所有消费该 Context 的后代组件。例如,假设我们有如下组件结构:

// App.js
import React from'react';
import MyContext from './MyContext';
import Parent from './Parent';

const App = () => {
  const sharedValue = 'from App';
  return (
    <MyContext.Provider value={sharedValue}>
      <Parent />
    </MyContext.Provider>
  );
};

export default App;

// Parent.js
import React from'react';
import Child from './Child';

const Parent = () => {
  return (
    <div>
      <Child />
    </div>
  );
};

export default Parent;

// Child.js
import React, { useContext } from'react';
import MyContext from './MyContext';

const Child = () => {
  const value = useContext(MyContext);
  return (
    <div>
      Value in Child: {value}
    </div>
  );
};

export default Child;

在这个例子中,App 组件作为 ProviderChild 组件消费 Context。React 会在组件树遍历过程中,为 Child 组件找到对应的 Provider,并将 Providervalue 传递给 Child

上下文对象的存储与更新

Context 的值存储在 React 内部的 Fiber 节点数据结构中。Fiber 是 React 16 引入的新的协调算法(reconciliation algorithm)的基础数据结构。每个组件在 React 内部都对应一个 Fiber 节点。当一个组件是 Context.Provider 时,其 value 会被存储在对应的 Fiber 节点上。

Providervalue 更新时,React 会触发一次重新渲染。这次重新渲染不仅仅是 Provider 自身,还会沿着 Context 传递路径影响到所有消费该 Context 的后代组件。React 通过比较 Provider 的新旧 value 来决定是否需要更新消费组件。默认情况下,React 使用 Object.is 方法进行比较。例如,如果 Providervalue 是一个对象,当对象的引用发生变化时(即使对象内部属性没有改变),消费组件也会重新渲染。以下代码展示了这种情况:

import React, { useState } from'react';
import MyContext from './MyContext';

const App = () => {
  const [data, setData] = useState({ message: 'initial' });
  const handleClick = () => {
    // 这里只是创建了一个新的对象引用,对象内容没有实质改变
    setData({ message: 'initial' });
  };
  return (
    <div>
      <button onClick={handleClick}>Update Context</button>
      <MyContext.Provider value={data}>
        {/* 子组件树 */}
      </MyContext.Provider>
    </div>
  );
};

export default App;

在这个例子中,每次点击按钮,data 的引用发生变化,导致消费该 Context 的组件重新渲染。

嵌套的 Context

在实际应用中,可能会出现多个 Context 嵌套的情况。例如,一个应用可能同时需要用户信息 Context 和主题信息 Context。在这种情况下,React 会按照组件树的层级顺序依次查找对应的 Provider。以下是一个简单的嵌套 Context 示例:

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

const UserContext = React.createContext();

export default UserContext;

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

const ThemeContext = React.createContext();

export default ThemeContext;

// App.js
import React, { useState } from'react';
import UserContext from './UserContext';
import ThemeContext from './ThemeContext';
import Child from './Child';

const App = () => {
  const [user, setUser] = useState({ name: 'John' });
  const [theme, setTheme] = useState('light');
  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Child />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
};

export default App;

// Child.js
import React, { useContext } from'react';
import UserContext from './UserContext';
import ThemeContext from './ThemeContext';

const Child = () => {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);
  return (
    <div>
      User: {user.name}, Theme: {theme}
    </div>
  );
};

export default Child;

在这个示例中,Child 组件同时消费了 UserContextThemeContext。React 会先查找最近的 UserContext.Provider,再查找最近的 ThemeContext.Provider,并将对应的值传递给 Child 组件。

Context 的性能考量

不必要的重新渲染

如前文所述,Providervalue 变化会导致消费组件重新渲染。这可能会带来性能问题,尤其是当 value 频繁变化或者消费组件树庞大时。为了避免不必要的重新渲染,可以采取以下几种方法:

  1. 减少 value 的变化频率:尽量保持 Providervalue 稳定。例如,如果 value 是一个对象,可以通过 Object.freeze 方法冻结对象,防止意外修改导致引用变化。
import React, { useState } from'react';
import MyContext from './MyContext';

const App = () => {
  const [data, setData] = useState({ message: 'initial' });
  const handleClick = () => {
    // 冻结对象,防止引用变化
    const newData = { message: 'updated' };
    Object.freeze(newData);
    setData(newData);
  };
  return (
    <div>
      <button onClick={handleClick}>Update Context</button>
      <MyContext.Provider value={data}>
        {/* 子组件树 */}
      </MyContext.Provider>
    </div>
  );
};

export default App;
  1. 使用 React.memo 包裹消费组件React.memo 是一个高阶组件,它可以对函数组件进行浅比较(shallow comparison),只有当组件的 props 发生变化时才会重新渲染。对于消费 Context 的组件,如果其只依赖 Context 的值,可以使用 React.memo 包裹。
import React, { useContext } from'react';
import MyContext from './MyContext';

const ChildComponent = () => {
  const value = useContext(MyContext);
  return (
    <div>
      The value from context is: {value}
    </div>
  );
};

export default React.memo(ChildComponent);
  1. 拆分 Context:如果可能,将不同变化频率的数据拆分到不同的 Context 中。这样可以避免一个数据的变化导致所有消费组件重新渲染。例如,将用户信息和应用配置信息分别放在不同的 Context 中。

与 Redux 等状态管理库的比较

Redux 是一个流行的状态管理库,与 React Context 有一些相似之处,都可以实现数据共享。然而,它们在设计理念和使用场景上存在差异:

  1. 数据流动方式
    • React Context:数据通过组件树传递,更适合在局部组件树内共享数据。例如,在一个特定的组件模块中共享用户偏好设置。
    • Redux:采用单向数据流,所有数据集中在一个 store 中,通过 actions 和 reducers 来更新状态。这种方式更适合管理应用的全局状态,如用户登录状态、购物车信息等。
  2. 性能优化
    • React Context:在避免不必要的重新渲染方面相对较弱,需要开发者手动优化。
    • Redux:通过使用 reselect 等库,可以实现更细粒度的状态选择和缓存,从而更好地控制组件的重新渲染。
  3. 复杂度
    • React Context:使用相对简单,适合轻量级的状态共享需求。
    • Redux:引入了更多的概念和样板代码(如 actions、reducers、store 等),适合大型复杂应用的状态管理,但学习成本较高。

Context 的使用场景

全局配置

在应用中,可能存在一些全局配置信息,如 API 地址、主题设置等。使用 Context 可以方便地在整个应用中共享这些配置。例如,一个多主题的应用,可以通过 Context 传递当前主题信息,各个组件根据主题信息渲染不同的样式。

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

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

export default ThemeContext;

// App.js
import React, { useState } from'react';
import ThemeContext from './ThemeContext';
import Child from './Child';

const App = () => {
  const [theme, setTheme] = useState('light');
  const handleThemeChange = () => {
    setTheme(theme === 'light'? 'dark' : 'light');
  };
  return (
    <div>
      <button onClick={handleThemeChange}>Toggle Theme</button>
      <ThemeContext.Provider value={theme}>
        <Child />
      </ThemeContext.Provider>
    </div>
  );
};

export default App;

// Child.js
import React, { useContext } from'react';
import ThemeContext from './ThemeContext';

const Child = () => {
  const theme = useContext(ThemeContext);
  return (
    <div>
      Current theme: {theme}
    </div>
  );
};

export default Child;

用户认证信息

在一个需要用户登录的应用中,用户认证信息(如用户名、用户 ID、权限等)可能需要在多个组件中使用。通过 Context 可以方便地将这些信息传递给需要的组件,而无需在每个组件间层层传递 props。

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

const AuthContext = React.createContext();

export default AuthContext;

// App.js
import React, { useState } from'react';
import AuthContext from './AuthContext';
import Page from './Page';

const App = () => {
  const [user, setUser] = useState({ name: 'John', role: 'admin' });
  return (
    <AuthContext.Provider value={user}>
      <Page />
    </AuthContext.Provider>
  );
};

export default App;

// Page.js
import React, { useContext } from'react';
import AuthContext from './AuthContext';

const Page = () => {
  const user = useContext(AuthContext);
  return (
    <div>
      User: {user.name}, Role: {user.role}
    </div>
  );
};

export default Page;

多语言支持

对于国际化的应用,需要在不同组件中根据用户设置的语言显示相应的文本。Context 可以用来共享当前语言设置,使得各个组件能够根据语言设置加载合适的翻译文本。

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

const LanguageContext = React.createContext('en');

export default LanguageContext;

// App.js
import React, { useState } from'react';
import LanguageContext from './LanguageContext';
import ComponentA from './ComponentA';

const App = () => {
  const [language, setLanguage] = useState('en');
  const handleLanguageChange = () => {
    setLanguage(language === 'en'? 'zh' : 'en');
  };
  return (
    <div>
      <button onClick={handleLanguageChange}>Change Language</button>
      <LanguageContext.Provider value={language}>
        <ComponentA />
      </LanguageContext.Provider>
    </div>
  );
};

export default App;

// ComponentA.js
import React, { useContext } from'react';
import LanguageContext from './LanguageContext';

const messages = {
  en: { greeting: 'Hello' },
  zh: { greeting: '你好' }
};

const ComponentA = () => {
  const language = useContext(LanguageContext);
  return (
    <div>
      {messages[language].greeting}
    </div>
  );
};

export default ComponentA;

通过以上对 React 中 Context 的详细介绍,包括其创建、消费、工作原理、性能考量以及使用场景,希望能帮助开发者更深入地理解和应用 Context,从而构建出更高效、灵活的 React 应用。