带你30分钟弄明白useContext的原理,教不会你随便喷!

前言

在React的函数组件中,useContext是一个非常重要的Hook,它让我们能够轻松地在组件树中共享状态。但是,你真的理解它的工作原理吗?

开宗明义:useContext的重新渲染原理

useContext能重新渲染的原理,其实就是订阅发布者模式!

  • 订阅阶段 :当使用useContext时,其实就是在订阅。订阅做的事情其实就是把这个fiber添加一些特殊的标记(dependencies)。
  • 发布阶段:等到发布者(Provider)的value发生改变时,就会执行发布这个行为。发布的行为就是给之前的特殊标记的fiber节点标记上更新。
  • 渲染阶段:其他事情交给React的渲染机制去做了。

这就是useContext重新渲染的完整流程!

目录

  1. 订阅与发布机制
  2. 渲染周期详解
  3. Fiber树结构与依赖关系
  4. 基本概念澄清与常见误区

订阅与发布机制

订阅机制

当函数组件调用useContext时,React会在订阅者组件的fiber上记录依赖关系:

javascript 复制代码
function useContext(Context) {
  const fiber = getCurrentFiber();
  
  if (fiber) {
    // 在订阅者组件的 fiber 上记录依赖关系
    if (!fiber.dependencies) {
      fiber.dependencies = [];
    }
    
    // 只需要注意这里,这里就添加了特殊标记
    fiber.dependencies.push({
      context: Context,
      fiber: fiber
    });
  }
  
  return Context._currentValue;
}

关键点 :订阅关系记录在订阅者组件的fiber上,而不是Provider的fiber上。

发布机制

当Provider的值发生变化时,React会遍历整个fiber树来找到所有订阅者,并且确保找到的订阅者订阅的是这个特定的发布者:

javascript 复制代码
function propagateContextChange(context, fiber) {
  let current = fiber.child;
  
  while (current !== null) {
    // 检查当前 fiber 是否订阅了目标 Context
    if (current.dependencies) {
      for (let i = 0; i < current.dependencies.length; i++) {
        const dependency = current.dependencies[i];
        if (dependency.context === context) {
          // 找到订阅者,标记需要更新
          scheduleUpdateOnFiber(current);
          break;
        }
      }
    }
    
    // 继续遍历子节点
    if (current.child !== null) {
      current = current.child;
      continue;
    }
    
    // 遍历兄弟节点
    while (current.sibling === null) {
      if (current.return === null || current.return === fiber) {
        return;
      }
      current = current.return;
    }
    current = current.sibling;
  }
}

重要理解

  • 遍历整个fiber树是为了找到全部的订阅者
  • 一个订阅者组件可以订阅多个发布者,比如:
javascript 复制代码
const Child = () => {
  const user = useContext(UserContext);    // 订阅 UserContext
  const theme = useContext(ThemeContext);  // 订阅 ThemeContext
  return <div>{user.name}</div>;
};

渲染周期详解

完整的渲染流程

让我们通过一个具体例子来理解整个渲染周期:

javascript 复制代码
const App = () => {
  const [count, setCount] = useState(0);
  
  return (
    <CountContext.Provider value={count}>
      <Child />
    </CountContext.Provider>
  );
};

const Child = () => {
  const count = useContext(CountContext);
  return <div>{count}</div>;
};

setCount(1)被调用时,完整的渲染流程如下:

1. 状态更新

javascript 复制代码
setCount(1); // 触发状态更新

2. React调度更新

javascript 复制代码
scheduleUpdateOnFiber(rootFiber); // 调度根fiber的更新

3. 开始渲染流程

javascript 复制代码
renderRootSync(root); // 开始同步渲染流程

4. 处理App组件

javascript 复制代码
updateFunctionComponent(appFiber);
// App 重新执行,Provider 接收到新的 value

5. 处理Provider

javascript 复制代码
updateContextProvider(providerFiber);
// 比较新旧值,发现不同,触发发布

6. 通知订阅者

javascript 复制代码
propagateContextChange(context, providerFiber);
// 找到 Child 组件,标记需要更新

7. 处理Child组件

javascript 复制代码
updateFunctionComponent(childFiber);
// Child 重新执行,useContext 返回新值

8. 渲染到DOM

javascript 复制代码
commitRoot(root); // 将更新提交到DOM

关键函数实现

updateContextProvider

javascript 复制代码
function updateContextProvider(current, workInProgress, Component, nextProps) {
  const context = workInProgress.type._context;
  const newValue = nextProps.value;
  const oldValue = current.memoizedState;
  
  // 比较新旧值
  if (is(oldValue, newValue)) {
    // 值相同,不更新
    return;
  }
  
  // 值不同,更新 Context 的值
  context._currentValue = newValue;
  
  // 标记需要更新
  workInProgress.flags |= Update;
  
  // 通知所有订阅者
  propagateContextChange(context, workInProgress);
}

Fiber树结构与依赖关系

为什么需要遍历整个fiber树

让我们通过一个具体的例子来说明:

javascript 复制代码
const App = () => {
  const [count, setCount] = useState(0);
  
  return (
    <CountContext.Provider value={count}>
      <div>
        <Header />
        <Main>
          <C1 /> {/* 订阅者 1 */}
        </Main>
        <Footer>
          <C2 /> {/* 订阅者 2 */}
        </Footer>
      </div>
    </CountContext.Provider>
  );
};

const C1 = () => {
  const count = useContext(CountContext);
  return <div>{count}</div>;
};

const C2 = () => {
  const count = useContext(CountContext);
  return <div>{count}</div>;
};

Fiber树结构

arduino 复制代码
// fiber 树结构:
// App
// └── div
//     ├── Header
//     ├── Main
//     │   └── C1 (订阅者)
//     └── Footer
//         └── C2 (订阅者)

// 需要遍历整个树才能找到 C1 和 C2

依赖关系分布

javascript 复制代码
// 每个组件的 dependencies 分布:

// App 组件(Provider)
appFiber.dependencies = [] 
// 说明:App 没有使用 useContext,只是 Provider

// Header 组件
headerFiber.dependencies = [] 
// 说明:Header 没有使用 useContext

// Main 组件
mainFiber.dependencies = [] 
// 说明:Main 没有使用 useContext

// C1 组件(订阅者)
c1Fiber.dependencies = [
  { context: CountContext, fiber: c1Fiber }
]
// 说明:C1 订阅了 CountContext

// Footer 组件
footerFiber.dependencies = [] 
// 说明:Footer 没有使用 useContext

// C2 组件(订阅者)
c2Fiber.dependencies = [
  { context: CountContext, fiber: c2Fiber }
]
// 说明:C2 订阅了 CountContext

遍历查找过程

javascript 复制代码
// 当 CountContext 值变化时,遍历过程:
function propagateContextChange(context, fiber) {
  let current = fiber.child; // 从 div 开始
  
  while (current !== null) {
    // 检查当前 fiber 的 dependencies
    if (current.dependencies) {
      for (let i = 0; i < current.dependencies.length; i++) {
        const dependency = current.dependencies[i];
        if (dependency.context === context) {
          // 找到订阅者!
          scheduleUpdateOnFiber(current);
          break;
        }
      }
    }
    
    // 继续遍历...
    if (current.child !== null) {
      current = current.child;
      continue;
    }
    
    // 遍历兄弟节点
    while (current.sibling === null) {
      if (current.return === null || current.return === fiber) {
        return;
      }
      current = current.return;
    }
    current = current.sibling;
  }
}

基本概念澄清与常见误区

重要纠正:函数组件的useContext

前提说明 :一些文章里说到通过fiber.tag === ContextConsumer判断是否是订阅者组件时,其实是在类组件里,在函数组件中不是这样。

在函数组件中:

javascript 复制代码
// 当组件使用 useContext 时
const Child = () => {
  const count = useContext(CountContext); // 这里只是获取值
  return <div>{count}</div>;
};

// 这个组件仍然是 FunctionComponent,不是 ContextConsumer
// fiber.tag === FunctionComponent

关键点useContext本身不会 创建ContextConsumer fiber。ContextConsumer实际上对应的是类组件时代的Context.Consumer

dependencies的真正含义

每个组件的dependencies只记录自己订阅了哪些Context:

javascript 复制代码
// 当组件使用 useContext 时
const C1 = () => {
  const count = useContext(CountContext); // C1 订阅了 CountContext
  return <div>{count}</div>;
};

const C2 = () => {
  const count = useContext(CountContext); // C2 也订阅了 CountContext
  return <div>{count}</div>;
};

// C1 的 fiber.dependencies = [
//   { context: CountContext, fiber: c1Fiber }
// ]

// C2 的 fiber.dependencies = [
//   { context: CountContext, fiber: c2Fiber }
// ]

// 注意:A 的 dependencies 是空的,因为 A 没有使用 useContext
// A 只是 Provider,不是消费者

认为有专门的订阅者数组

javascript 复制代码
// ❌ 错误:认为 React 维护了订阅者数组
// CountContext.subscribers = [C1, C2]

// ✅ 正确:React 通过遍历 fiber 树来找到订阅者
// 每个组件的 dependencies 只记录自己订阅了哪些 Context

总结

通过本文的深入解析,我们了解了useContext的核心原理:

  1. 订阅机制 :组件调用useContext时,在订阅者组件的fiber上记录依赖关系
  2. 发布机制:Provider值变化时,遍历fiber树找到所有订阅者,确保找到的订阅者订阅的是这个特定的发布者
  3. 渲染流程:从状态更新到DOM渲染的完整8步流程
  4. 依赖关系 :每个组件的dependencies只记录自己订阅的Context
  5. 常见误区:澄清了dependencies的真正含义和遍历的必要性

useContext的设计体现了React的响应式理念,通过fiber树的天然结构实现了高效的跨组件状态共享。理解这些原理,不仅有助于我们更好地使用Context,也能在面试中展现出对React内部机制的深入理解。


记住 :在函数组件中,useContext不会创建ContextConsumer fiber,而是通过dependencies记录订阅关系,通过遍历fiber树来找到所有订阅者。这是React Context机制的核心所在。

相关推荐
猫头_22 分钟前
uni-app 转微信小程序 · 避坑与实战全记录
前端·微信小程序·uni-app
天生我材必有用_吴用25 分钟前
网页接入弹窗客服功能的完整实现(Vue3 + WebSocket预备方案)
前端
海拥31 分钟前
8 Ball Pool:在浏览器里打一局酣畅淋漓的桌球!
前端
Cache技术分享38 分钟前
148. Java Lambda 表达式 - 捕获局部变量
前端·后端
YGY Webgis糕手之路42 分钟前
Cesium 快速入门(二)底图更换
前端·经验分享·笔记·vue
神仙别闹1 小时前
基于JSP+MySQL 实现(Web)毕业设计题目收集系统
java·前端·mysql
前端李二牛1 小时前
Web字体使用最佳实践
前端·http
YGY_Webgis糕手之路1 小时前
Cesium 快速入门(六)实体类型介绍
前端·gis·cesium
Jacob02341 小时前
UI 代码不写也行?我用 MCP Server 和 ShadCN 自动生成前端界面
前端·llm·ai编程
爱泡脚的鸡腿1 小时前
Vue第五次笔记
前端·vue.js