带你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树结构

text
复制代码
// 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机制的核心所在。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP