Appearance
第八章:深入 Context API
8.1 Context API:基本概念与用法
在 React 的组件树中,数据通常通过 props 自上而下地传递。然而,当组件层级非常深时,这种方式会导致所谓的“属性钻探”(Props Drilling)问题——许多中间层组件仅仅是为了向下传递属性而存在,它们自身并不消费这些属性。这使得组件的耦合度增高,复用性降低,代码也变得冗长和难以维护。
为了解决这个问题,React 引入了 Context API。它提供了一种在组件之间共享“全局”数据的方式,而无需显式地通过组件树的每一层手动传递 props。
createContext:创建上下文
一切始于 createContext 函数。这个函数创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,它将从组件树中离自身最近的那个匹配的 Provider 中读取当前的 context 值。
让我们深入源码,看看 createContext 到底做了什么。
源码路径: packages/react/src/ReactContext.js
javascript
import {REACT_CONSUMER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import type {ReactContext} from 'shared/ReactTypes';
export function createContext<T>(defaultValue: T): ReactContext<T> {
// TODO: Second argument used to be an optional `calculateChangedBits`
// function. Warn to reserve for future use?
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
_currentValue: defaultValue,
_currentValue2: defaultValue,
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_threadCount: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
};
context.Provider = context;
context.Consumer = {
$$typeof: REACT_CONSUMER_TYPE,
_context: context,
};
return context;
}从源码中我们可以看到,createContext 函数接收一个 defaultValue 参数,并返回一个 context 对象。这个对象包含了几个关键属性:
$$typeof: 一个 Symbol (REACT_CONTEXT_TYPE),用于将Context对象标识为 React 的内部类型。_currentValue/_currentValue2: 这两个字段存储了Context的当前值。_currentValue用于主渲染器(如 React DOM),而_currentValue2是为了支持并发渲染器(如 React Native 和 Fabric)而设计的备用字段。它们的初始值都是你传入的defaultValue。Provider: 一个组件,允许消费组件订阅 context 的变化。我们注意到context.Provider = context,这意味着Provider属性直接指向了context对象本身。Consumer: 一个组件,用于在类组件中订阅 context。它内部通过_context属性指回context对象。
defaultValue 参数是一个很重要的备选项:当一个组件在组件树中没有找到对应的 Provider 时,它就会使用这个默认值。
Provider 与 Consumer
createContext 返回的 context 对象附带了两个重要的组件:Provider 和 Consumer。
Provider(提供者): 每个Context对象都会返回一个ProviderReact 组件,它允许消费组件订阅 context 的变化。Provider接收一个value属性,传递给消费组件。一个Provider可以和多个消费组件有对应关系。多个Provider也可以嵌套使用,里层的会覆盖外层的数据。Consumer(消费者): 在函数组件中,我们通常使用useContextHook 来消费 context。在类组件中,或者在不支持 Hook 的场景下,可以使用Consumer组件。它需要一个函数作为子元素(function as a child),这个函数接收当前的 context 值,并返回一个 React 节点。
基本用法示例
让我们来看一个简单的主题切换示例,展示如何使用 createContext 和 useContext。
javascript
import React, { useState, useContext, createContext } from 'react';
// 1. 创建一个 Context 对象
const ThemeContext = createContext('light'); // 默认值为 'light'
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
// 2. 使用 Provider 为后代组件提供 value
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 3. 使用 useContext Hook 来消费 context
const { theme, toggleTheme } = useContext(ThemeContext);
const style = {
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
border: '1px solid',
};
return (
<button style={style} onClick={toggleTheme}>
I am a {theme} button
</button>
);
}在这个例子中:
- 我们用
createContext('light')创建了一个ThemeContext,默认主题是'light'。 - 在
App组件中,我们使用<ThemeContext.Provider>将当前的theme值和toggleTheme函数向下传递。 ThemedButton组件(Toolbar的子组件)通过useContext(ThemeContext)直接获取了theme和toggleTheme,而无需Toolbar组件进行任何 props 传递。
通过这种方式,Context API 优雅地解决了跨层级组件通信的问题,使得代码结构更加清晰和可维护。在下一节中,我们将深入探讨 Provider 是如何工作的。