Skip to content

深入理解 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 为什么不适合高频更新的状态管理?

  1. 无选择器:无法只订阅 value 的某个字段,整个 value 变化时所有消费者都重渲染
  2. 穿透 memo:不像 Zustand/Jotai 的 selector 可以被 memo 阻止
  3. Provider 层级:多个 Context 嵌套导致组件树层级加深(Provider Hell)

Context 适合低频变化的配置(主题、语言、认证状态),高频状态应使用 Zustand/Jotai 等专用库。