深入理解 Context 源码
createContext 原理、Provider/Consumer 的 Fiber 处理、传播机制与为什么跳过 bailout
什么是 Context?
定义:Context 是 React 内置的跨层级数据传递机制,允许数据跳过中间组件直接到达深层消费者。它本质上是一个依赖注入系统,而非状态管理工具。
一、createContext 内部结构
javascript
function createContext(defaultValue) {
const context = {
$$typeof: REACT_CONTEXT_TYPE,
// Provider 组件
Provider: null,
// Consumer 组件(旧 API)
Consumer: null,
// 当前值(用于读取)
_currentValue: defaultValue, // 主线程用
_currentValue2: defaultValue, // 并发渲染用(secondary renderer)
_threadCount: 0,
};
// Provider 的 type 指向 context 自身
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
// Consumer 指向 context
context.Consumer = context;
return context;
}
// 使用:
const ThemeContext = createContext('light');
// ThemeContext._currentValue = 'light'
// ThemeContext.Provider._context === ThemeContext二、Provider 的 Fiber 处理
beginWork 中的 ContextProvider
javascript
function updateContextProvider(current, workInProgress, renderLanes) {
const context = workInProgress.type._context; // 拿到 context 对象
const newValue = workInProgress.pendingProps.value;
const oldValue = current?.memoizedProps?.value;
// 1. 更新 context 的当前值
pushProvider(workInProgress, context, newValue);
// → context._currentValue = newValue
if (current !== null) {
if (Object.is(oldValue, newValue)) {
// 值没变 → 走正常的 bailout 流程
// 但注意:即使 Provider bailout,Consumer 仍会检查
} else {
// 值变了!→ 向下传播变化
propagateContextChange(workInProgress, context, renderLanes);
}
}
// 继续 reconcile 子节点
reconcileChildren(current, workInProgress, newProps.children, renderLanes);
return workInProgress.child;
}propagateContextChange —— 核心传播机制
javascript
function propagateContextChange(provider, context, renderLanes) {
// 从 Provider 向下遍历整棵子 Fiber 树
let fiber = provider.child;
while (fiber !== null) {
// 检查这个 Fiber 是否依赖了当前 context
const dependencies = fiber.dependencies;
if (dependencies !== null) {
let dep = dependencies.firstContext;
while (dep !== null) {
if (dep.context === context) {
// 找到依赖此 context 的消费者!
// 标记这个 Fiber 需要更新
fiber.lanes |= renderLanes;
// 向上标记祖先的 childLanes
let parent = fiber.return;
while (parent !== null) {
parent.childLanes |= renderLanes;
// 同时标记 alternate
if (parent.alternate) {
parent.alternate.childLanes |= renderLanes;
}
parent = parent.return;
}
break;
}
dep = dep.next;
}
}
// 继续遍历(深度优先)
if (fiber.child !== null) {
fiber = fiber.child;
} else {
while (fiber !== null) {
if (fiber === provider) return; // 回到 Provider,结束
if (fiber.sibling !== null) {
fiber = fiber.sibling;
break;
}
fiber = fiber.return;
}
}
}
}关键:propagateContextChange 直接遍历 Fiber 子树标记 lanes,这就是为什么 Context 变化时中间组件的 React.memo 无法阻止消费者重渲染——消费者的 lanes 被直接标记了,bailout 检查会发现有待处理的更新。
三、useContext 的实现
javascript
function useContext(context) {
// 1. 读取当前值
const value = context._currentValue;
// 2. 建立依赖关系
// 将 context 加入当前 Fiber 的 dependencies 链表
const dependency = {
context: context,
next: null,
};
if (lastContextDependency === null) {
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: dependency,
};
} else {
lastContextDependency.next = dependency;
}
lastContextDependency = dependency;
// 3. 返回值
return value;
}依赖链表的作用
组件中使用了多个 Context:
const theme = useContext(ThemeContext);
const locale = useContext(LocaleContext);
const auth = useContext(AuthContext);
Fiber.dependencies:
firstContext → { context: ThemeContext }
→ { context: LocaleContext }
→ { context: AuthContext }
当任何一个 Context 的 Provider 值变化时:
propagateContextChange 遍历子树
检查每个 Fiber 的 dependencies 链表
如果包含变化的 context → 标记更新四、为什么 Context 变化会跳过 bailout?
正常 bailout 流程
javascript
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) {
if (oldProps === newProps &&
!hasContextChanged() &&
(current.lanes & renderLanes) === 0) {
// 三个条件都满足 → bailout
return bailoutOnAlreadyFinishedWork();
}
}
}Context 变化时的情况
Provider value 变化
↓
propagateContextChange 遍历子树
↓
找到 Consumer → 直接标记 fiber.lanes |= renderLanes
↓
同时向上标记所有祖先的 childLanes
↓
即使中间有 React.memo 的组件:
该组件的 props 没变 → 本身 bailout ✅
但 childLanes 被标记了 → 子树需要继续检查 ✅
↓
最终到达 Consumer 组件
lanes 被标记 → (current.lanes & renderLanes) !== 0
→ bailout 条件不满足 → 执行更新示意图
jsx
<ThemeContext.Provider value={theme}> ← value 变化
<div> ← childLanes 被标记
<MemoWrapper> ← props 没变,自身 bailout
<div> ← childLanes 被标记,继续向下
<ThemeConsumer /> ← lanes 被标记,重新渲染
</div>
</MemoWrapper>
</div>
</ThemeContext.Provider>
// MemoWrapper 自身不渲染,但因为 childLanes 被标记,
// React 仍会继续遍历到 ThemeConsumer五、Provider 栈机制
javascript
// React 用栈管理嵌套的 Provider 值
const valueStack = [];
let index = -1;
function pushProvider(fiber, context, value) {
index++;
valueStack[index] = context._currentValue; // 保存旧值
context._currentValue = value; // 设置新值
}
function popProvider(fiber, context) {
context._currentValue = valueStack[index]; // 恢复旧值
valueStack[index] = null;
index--;
}
// 嵌套 Provider 示例:
// <ThemeContext.Provider value="dark"> pushProvider → stack: ['light']
// <ThemeContext.Provider value="blue"> pushProvider → stack: ['light', 'dark']
// <Child /> _currentValue = 'blue'
// </ThemeContext.Provider> popProvider → _currentValue = 'dark'
// </ThemeContext.Provider> popProvider → _currentValue = 'light'六、Context 的性能问题与优化
问题:所有消费者都重渲染
jsx
const AppContext = createContext({ theme: 'light', locale: 'zh', user: null });
function App() {
const [state, setState] = useState({ theme: 'light', locale: 'zh', user: null });
return (
<AppContext.Provider value={state}>
{/* 任何一个属性变化 → 所有消费者重渲染 */}
<ThemeDisplay /> {/* 只用了 theme */}
<LocaleDisplay /> {/* 只用了 locale */}
<UserDisplay /> {/* 只用了 user */}
</AppContext.Provider>
);
}优化1:拆分 Context
jsx
const ThemeContext = createContext('light');
const LocaleContext = createContext('zh');
const UserContext = createContext(null);
function App() {
const [theme, setTheme] = useState('light');
const [locale, setLocale] = useState('zh');
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={theme}>
<LocaleContext.Provider value={locale}>
<UserContext.Provider value={user}>
<ThemeDisplay /> {/* 只订阅 ThemeContext */}
<LocaleDisplay /> {/* 只订阅 LocaleContext */}
<UserDisplay /> {/* 只订阅 UserContext */}
</UserContext.Provider>
</LocaleContext.Provider>
</ThemeContext.Provider>
);
}优化2:useMemo 稳定 value 引用
jsx
function App() {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
// ❌ 每次 App 渲染都创建新对象 → 所有消费者重渲染
return <ThemeContext.Provider value={{ theme, setTheme }}>...</ThemeContext.Provider>;
// ✅ useMemo 稳定引用
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext.Provider value={value}>...</ThemeContext.Provider>;
// count 变化时 value 引用不变 → 消费者不重渲染
}优化3:状态与 dispatch 分离
jsx
const StateContext = createContext(null); // 值变化频繁
const DispatchContext = createContext(null); // 函数引用稳定
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
}
// 只需要 dispatch 的组件不会因为 state 变化而重渲染
function AddButton() {
const dispatch = useContext(DispatchContext); // dispatch 引用稳定
return <button onClick={() => dispatch({ type: 'add' })}>添加</button>;
}面试高频题
Q: Context 变化时 React.memo 能阻止消费者重渲染吗?
不能。Context 变化时,propagateContextChange 会直接遍历 Fiber 子树,在消费者 Fiber 上标记 lanes。即使中间组件用了 React.memo 实现了 bailout,React 仍然通过 childLanes 知道子树中有需要更新的节点,会继续向下遍历直到消费者。
Q: useContext 是怎么知道值变了的?
useContext 在渲染时将 context 加入 Fiber 的 dependencies 链表。当 Provider 的值变化时,propagateContextChange 遍历子树,检查每个 Fiber 的 dependencies 是否包含该 context,包含则标记 lanes。下次 beginWork 时发现 lanes 不为空,跳过 bailout,重新执行组件函数,useContext 读到新的 _currentValue。
Q: Context 为什么不适合高频更新的状态管理?
- 无选择器:无法只订阅 value 的某个字段,整个 value 变化时所有消费者都重渲染
- 穿透 memo:不像 Zustand/Jotai 的 selector 可以被 memo 阻止
- Provider 层级:多个 Context 嵌套导致组件树层级加深(Provider Hell)
Context 适合低频变化的配置(主题、语言、认证状态),高频状态应使用 Zustand/Jotai 等专用库。