带你30分钟弄明白useContext的原理,教不会你随便喷!
前言
在React的函数组件中,useContext是一个非常重要的Hook,它让我们能够轻松地在组件树中共享状态。但是,你真的理解它的工作原理吗?
开宗明义:useContext的重新渲染原理
useContext能重新渲染的原理,其实就是订阅发布者模式!
- 订阅阶段:当使用
useContext时,其实就是在订阅。订阅做的事情其实就是把这个fiber添加一些特殊的标记(dependencies)。 - 发布阶段:等到发布者(Provider)的value发生改变时,就会执行发布这个行为。发布的行为就是给之前的特殊标记的fiber节点标记上更新。
- 渲染阶段:其他事情交给React的渲染机制去做了。
这就是useContext重新渲染的完整流程!
目录
订阅与发布机制
订阅机制
当函数组件调用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的核心原理:
- 订阅机制:组件调用
useContext时,在订阅者组件的fiber上记录依赖关系 - 发布机制:Provider值变化时,遍历fiber树找到所有订阅者,确保找到的订阅者订阅的是这个特定的发布者
- 渲染流程:从状态更新到DOM渲染的完整8步流程
- 依赖关系:每个组件的
dependencies只记录自己订阅的Context - 常见误区:澄清了dependencies的真正含义和遍历的必要性
useContext的设计体现了React的响应式理念,通过fiber树的天然结构实现了高效的跨组件状态共享。理解这些原理,不仅有助于我们更好地使用Context,也能在面试中展现出对React内部机制的深入理解。
记住:在函数组件中,useContext不会创建ContextConsumer fiber,而是通过dependencies记录订阅关系,通过遍历fiber树来找到所有订阅者。这是React Context机制的核心所在。
