拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

图解 React 源码:Context 原理——从 fiber 树构造视角剖析 Provider 与 Consumer 实现机制

图解 React 源码:Context 原理——从 fiber 树构造视角剖析 Provider 与 Consumer 实现机制 教程前端【免费下载链接】react-illustration-series图解react源码, 用大量配图的方式, 致力于将react原理表述清楚.项目地址https://gitcode.com/gh_mirrors/re/react-illustration-series点击查看免费下载本篇技术指南以本仓库 context 原理 一文为骨架基于react17.0.2源码从fiber树构造的视角完整剖析 React Context 的实现原理。读完本文你将掌握createContext的数据结构设计、3 种消费方式如何统一收敛到readContext、pushProvider/popProvider的栈式状态管理以及更新阶段propagateContextChange如何精准定位所有 Consumer 并驱动其重新构造。Context 是什么简单来讲Context提供了一种直接访问祖先节点上状态的方法避免了多级组件层层传递props。有关Context的用法请直接查看官方文档。本文从fiber树构造的视角分析Context的实现原理。在 React 运行时中Context的实现跨越了两个核心阶段初次创建 / 对比更新时的fiber树构造由ContextProvider类型的 fiber 节点负责更新context._currentValue供消费由ContextConsumer类型的节点负责读取该值消费。更新阶段ContextProvider负责查找所有依赖该context的 Consumer 节点并通过设置父路径上各节点的fiber.childLanes保证消费节点能够进入更新流程。创建 ContextcreateContext 的数据结构根据官网示例通过React.createContext这个 API 来创建context对象。在createContext的实现中可以看到context对象的数据结构export function createContextT( defaultValue: T, calculateChangedBits: ?(a: T, b: T) number, ): ReactContextT { if (calculateChangedBits undefined) { calculateChangedBits null; } const context: ReactContextT { $$typeof: REACT_CONTEXT_TYPE, _calculateChangedBits: calculateChangedBits, // As a workaround to support multiple concurrent renderers, we categorize // some renderers as primary and others as secondary. We only expect // there to be two concurrent renderers at most: React Native (primary) and // Fabric (secondary); React DOM (primary) and React ART (secondary). // Secondary renderers store their context values on separate fields. _currentValue: defaultValue, _currentValue2: defaultValue, _threadCount: 0, Provider: (null: any), Consumer: (null: any), }; context.Provider { $$typeof: REACT_PROVIDER_TYPE, _context: context, }; context.Consumer context; return context; }createContext核心逻辑其初始值保存在context._currentValue同时保存到context._currentValue2。英文注释已经解释保存 2 个 value 是为了支持多个渲染器并发渲染——如 React DOM 与 React ART 可各自持有独立的值互不干扰。同时创建了context.Provider、context.Consumer2 个reactElement对象。其中Provider是一个$$typeof为REACT_PROVIDER_TYPE的对象内部通过_context指回context本身而Consumer则直接复用了context对象本身这也是为什么MyContext.Consumer在渲染时会被识别为ContextConsumer类型的节点。比如创建const MyContext React.createContext(defaultValue);之后使用MyContext.Provider value{/* 某个值 */}声明一个ContextProvider类型的组件。在 JSX 编译后Provider与Consumer都会以ReactElement的形式出现在ReactElement树中进而驱动fiber树的构造三者关系可参考 fiber 树构造(基础准备)。初次创建beginWork 中的 updateContextProvider在fiber树渲染时beginWork中ContextProvider类型的节点对应的处理函数是updateContextProviderfunction beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { const updateLanes workInProgress.lanes; workInProgress.lanes NoLanes; // ...省略无关代码 switch (workInProgress.tag) { case ContextProvider: return updateContextProvider(current, workInProgress, renderLanes); case ContextConsumer: return updateContextConsumer(current, workInProgress, renderLanes); } } function updateContextProvider( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ) { // ...省略无关代码 const providerType: ReactProviderTypeany workInProgress.type; const context: ReactContextany providerType._context; const newProps workInProgress.pendingProps; const oldProps workInProgress.memoizedProps; // 接收新value const newValue newProps.value; // 更新 ContextProvider._currentValue pushProvider(workInProgress, newValue); if (oldProps ! null) { // ... 省略更新context的逻辑, 下文讨论 } const newChildren newProps.children; reconcileChildren(current, workInProgress, newChildren, renderLanes); return workInProgress.child; }updateContextProvider()在fiber初次创建时十分简单仅仅就是保存了pendingProps.value做为context的最新值之后这个最新的值用于供给消费。随后继续调用reconcileChildren向下派生子节点走常规的fiber树构造流程。下图展示了beginWork阶段执行pushProvider后ThemeContext._currentValue由旧值default theme变为新值initial theme的过程context._currentValue 存储pushProvider / popProvider注意updateContextProvider - pushProvider中的pushProvider(workInProgress, newValue)// ...省略无关代码 export function pushProviderT(providerFiber: Fiber, nextValue: T): void { const context: ReactContextT providerFiber.type._context; push(valueCursor, context._currentValue, providerFiber); context._currentValue nextValue; }pushProvider实际上是一个存储函数利用栈的特性先把context._currentValue压栈之后更新context._currentValue nextValue。与pushProvider对应的还有popProvider同样利用栈的特性把栈中的值弹出还原到context._currentValue中export function popProvider(providerFiber: Fiber): void { const currentValue valueCursor.current; pop(valueCursor, providerFiber); const context: ReactContextany providerFiber.type._context; context._currentValue currentValue; }这一对函数配合fiber树构造的深度优先遍历特性天然契合beginWork阶段向下探寻时遇到Provider节点即入栈pushProvidercompleteWork阶段向上回溯时遇到Provider节点即出栈popProvider从而保证在遍历任意分支时context._currentValue始终保存的是当前分支上最近一次 Provider 设置的值。底层栈的实现定义于ReactFiberStack.js如下export type StackCursorT {| current: T |}; // 维护一个全局stack const valueStack: Arrayany []; let index -1; // 一个工厂函数, 创建StackCursor对象 function createCursorT(defaultValue: T): StackCursorT { return { current: defaultValue, }; } function isEmpty(): boolean { return index -1; } // 出栈 function popT(cursor: StackCursorT, fiber: Fiber): void { if (index 0) { return; } cursor.current valueStack[index]; valueStack[index] null; index--; } // 入栈 function pushT(cursor: StackCursorT, value: T, fiber: Fiber): void { index; // 注意: 这里存储的是 cursor当前值, 随后更新了cursor.current为 valueStack[index] cursor.current; cursor.current value; }其中valueStack是全局数组用于存储所有StackCursor.current不仅包括context api相关的valueCursor还包括reactFiberContext、reactFiberHostContext等其他模块的游标。StackCursor是一个泛型对象与context api相关的valueCursor定义如下// 定义全局 valueCursor, 用于管理Context.Provider/组件的value const valueCursor: StackCursormixed createCursor(null);下图展示了updateContextProvider执行时各StackCursor与右侧valueStack的联动状态valueCursor.current更新为新值而valueStack中按颜色一一对应的位置存入了变化前的旧值供回溯时恢复本节重点分析Context Api在fiber树构造过程中的作用。有关pushProvider/popProvider的具体实现过程栈存储在 React 算法之栈操作 中有详细图解其中还给出了一个三级嵌套Provider/Consumer的演示示例完整展示了MyContext对象在栈中的变化情况beginWork阶段入栈——每当遇到Context.Provider类型的节点则执行pushProvider配图。completeWork阶段出栈——每当遇到Context.Provider类型的节点则执行popProvider配图。reconciler结束后valueStack、valueCursor以及MyContext都恢复到了初始状态。由于reconciler过程是一个深度优先遍历过程对于fiber树来讲向下探寻beginWork阶段和向上回溯completeWork阶段天然就和栈的入栈push和出栈pop能够无缝配合Context 机制就是在这个特性上建立起来的。消费 Context3 种方式统一收敛到 readContext使用了MyContext.Provider组件之后在fiber树构造过程中context 的值会被ContextProvider类型的fiber节点所更新。在后续的过程中如何读取context._currentValue在react中共提供了 3 种方式可以消费Context使用MyContext.Consumer组件用于JSX。如MyContext.Consumer(value){}/MyContext.ConsumerbeginWork中对于ContextConsumer类型的节点对应的处理函数是updateContextConsumerfunction updateContextConsumer( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ) { let context: ReactContextany workInProgress.type; const newProps workInProgress.pendingProps; const render newProps.children; // 读取context prepareToReadContext(workInProgress, renderLanes); const newValue readContext(context, newProps.unstable_observedBits); let newChildren; // ...省略无关代码 }使用useContext用于function组件中。如const value useContext(MyContext)进入updateFunctionComponent后会调用prepareToReadContext无论是初次创建阶段还是更新阶段useContext都直接调用了readContext具体调用链可参考 hook 原理(概览) 中关于 Hook 与 fiber 状态的关系说明class组件中使用一个静态属性contextType用于class组件中获取context。如MyClass.contextType MyContext;进入updateClassComponent后会调用prepareToReadContext无论constructClassInstance、mountClassInstance、updateClassInstance内部都调用context readContext((contextType: any));所以这 3 种方式只是react根据不同使用场景封装的api内部都会调用prepareToReadContext和readContext(contextType)// ... 省略无关代码 export function prepareToReadContext( workInProgress: Fiber, renderLanes: Lanes, ): void { // 1. 设置全局变量, 为readContext做准备 currentlyRenderingFiber workInProgress; lastContextDependency null; lastContextWithAllBitsObserved null; const dependencies workInProgress.dependencies; if (dependencies ! null) { const firstContext dependencies.firstContext; if (firstContext ! null) { if (includesSomeLane(dependencies.lanes, renderLanes)) { // Context list has a pending update. Mark that this fiber performed work. markWorkInProgressReceivedUpdate(); } // Reset the work-in-progress list dependencies.firstContext null; } } } // ... 省略无关代码 export function readContextT( context: ReactContextT, observedBits: void | number | boolean, ): T { const contextItem { context: ((context: any): ReactContextmixed), observedBits: resolvedObservedBits, next: null, }; // 1. 构造一个contextItem, 加入到 workInProgress.dependencies链表之后 if (lastContextDependency null) { lastContextDependency contextItem; currentlyRenderingFiber.dependencies { lanes: NoLanes, firstContext: contextItem, responders: null, }; } else { lastContextDependency lastContextDependency.next contextItem; } // 2. 返回 currentValue return isPrimaryRenderer ? context._currentValue : context._currentValue2; }核心逻辑prepareToReadContext设置currentlyRenderingFiber workInProgress并重置lastContextDependency等全局变量。如果该 fiber 上一次的dependencies中存在 context 依赖且对应的lanes与本次渲染优先级相交则调用markWorkInProgressReceivedUpdate标记该 fiber 接收到了更新随后清空dependencies.firstContext为本次读取重新记录依赖。readContext返回context._currentValue并构造一个contextItem添加到workInProgress.dependencies链表之后。注意这个readContext并不是纯函数它还有一些副作用会更改workInProgress.dependencies其中contextItem.context保存了当前context的引用。这个dependencies属性会在更新时使用用于判定是否依赖了ContextProvider中的值——这正是propagateContextChange能够精准定位 Consumer 的依据下文展开。此外contextItem.observedBits记录的是本次读取时的观察位配合_calculateChangedBits计算出的changedBits可以做细粒度的按位订阅优化unstable_observedBits场景。返回context._currentValue之后之后继续进行fiber树构造直到全部完成即可。更新 ContextpropagateContextChange 的传播机制来到更新阶段同样进入updateContextConsumer实际更新入口是updateContextProviderfunction updateContextProvider( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ) { const providerType: ReactProviderTypeany workInProgress.type; const context: ReactContextany providerType._context; const newProps workInProgress.pendingProps; const oldProps workInProgress.memoizedProps; const newValue newProps.value; pushProvider(workInProgress, newValue); if (oldProps ! null) { // 更新阶段进入 const oldValue oldProps.value; // 对比 newValue 和 oldValue const changedBits calculateChangedBits(context, newValue, oldValue); if (changedBits 0) { // value没有变动, 进入 Bailout 逻辑 if ( oldProps.children newProps.children !hasLegacyContextChanged() ) { return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); } } else { // value变动, 查找对应的consumers, 并使其能够被更新 propagateContextChange(workInProgress, context, changedBits, renderLanes); } } // ... 省略无关代码 }核心逻辑value没有改变直接进入Bailout可以回顾 fiber 树构造(对比更新) 中对bailout的解释bailout用于判断子树节点是否完全复用如果可以复用则会略过 fiber 树构造。value改变调用propagateContextChange。propagateContextChange的核心实现如下export function propagateContextChange( workInProgress: Fiber, context: ReactContextmixed, changedBits: number, renderLanes: Lanes, ): void { let fiber workInProgress.child; if (fiber ! null) { // Set the return pointer of the child to the work-in-progress fiber. fiber.return workInProgress; } while (fiber ! null) { let nextFiber; const list fiber.dependencies; if (list ! null) { nextFiber fiber.child; let dependency list.firstContext; while (dependency ! null) { // 检查 dependency中依赖的context if ( dependency.context context (dependency.observedBits changedBits) ! 0 ) { // 符合条件, 安排调度 if (fiber.tag ClassComponent) { // class 组件需要创建一个update对象, 添加到updateQueue队列 const update createUpdate( NoTimestamp, pickArbitraryLane(renderLanes), ); update.tag ForceUpdate; // 注意ForceUpdate, 保证class组件一定执行render enqueueUpdate(fiber, update); } fiber.lanes mergeLanes(fiber.lanes, renderLanes); const alternate fiber.alternate; if (alternate ! null) { alternate.lanes mergeLanes(alternate.lanes, renderLanes); } // 向上 scheduleWorkOnParentPath(fiber.return, renderLanes); // 标记优先级 list.lanes mergeLanes(list.lanes, renderLanes); // 退出查找 break; } dependency dependency.next; } } // ...省略无关代码 // ...省略无关代码 fiber nextFiber; } }propagateContextChange源码比较长核心逻辑如下向下遍历从ContextProvider类型的节点开始向下查找所有fiber.dependencies依赖该context的节点假设叫做consumer。匹配条件是dependency.context context且(dependency.observedBits changedBits) ! 0即消费方确实订阅了这个 context 且本次变化位与之相关。向上遍历从consumer节点开始向上遍历修改父路径上所有节点的fiber.childLanes属性表明其子节点有改动子节点会进入更新逻辑。这一步通过调用scheduleWorkOnParentPath(fiber.return, renderLanes)实现export function scheduleWorkOnParentPath( parent: Fiber | null, renderLanes: Lanes, ) { // Update the child lanes of all the ancestors, including the alternates. let node parent; while (node ! null) { const alternate node.alternate; if (!isSubsetOfLanes(node.childLanes, renderLanes)) { node.childLanes mergeLanes(node.childLanes, renderLanes); if (alternate ! null) { alternate.childLanes mergeLanes( alternate.childLanes, renderLanes, ); } } else if ( alternate ! null !isSubsetOfLanes(alternate.childLanes, renderLanes) ) { alternate.childLanes mergeLanes(alternate.childLanes, renderLanes); } else { // Neither alternate was updated, which means the rest of the // ancestor path already has sufficient priority. break; } node node.return; } }scheduleWorkOnParentPath与markUpdateLaneFromFiberToRoot的作用相似都是沿return指针向上修改祖先的childLanes且同时处理alternate双缓冲树具体可以回顾 fiber 树构造(对比更新)。区别在于markUpdateLaneFromFiberToRoot是从发起更新的 fiber 一路标记到HostRootFiber而scheduleWorkOnParentPath只从 consumer 向上标记到 Provider 的父级路径作用范围更局部。在向下遍历时还注意一个细节对于匹配上的ClassComponent类型的 consumer除了设置fiber.lanes之外还会创建一个update.tag ForceUpdate的 update 对象并enqueueUpdate(fiber, update)。这是为了绕过shouldComponentUpdate等类组件的更新拦截机制强制class 组件在 context 变化时一定执行render从而保证状态一致性。下图展示了propagateContextChange的完整流程从发生value变动的Provider (theme)子节点开始向下遍历 fiber 树找到消费ThemeContext的Consumer (theme)节点后再向上沿父路径标记childLanes保证所有依赖该 Context 的组件都能在本次渲染中更新通过以上 2 个步骤保证了所有消费该context的子节点都会被重新构造进而保证了状态的一致性实现了context更新。值得补充的是propagateContextChange只是标记阶段的工作。真正让 consumer 重新执行beginWork的是后续fiber树构造循环中的判断——当遍历到被标记了childLanes的祖先节点时beginWork中的bailout判断!includesSomeLane(renderLanes, workInProgress.childLanes)会认为子节点需要更新从而clone并继续向下构造详见 fiber 树构造(对比更新)最终使ContextConsumer/ 消费组件重新执行readContext读取到最新值。总结Context的实现思路还是比较清晰总体分为 2 步消费状态时ContextConsumer节点调用readContext(MyContext)获取最新状态。readContext除了返回context._currentValue之外还会把contextItem挂到workInProgress.dependencies链表上为更新阶段提供哪些节点依赖了该 context的依据。3 种消费方式Context.Consumer、useContext、contextType最终都收敛到prepareToReadContextreadContext这一对函数。更新状态时由ContextProvider节点负责查找所有ContextConsumer节点并设置消费节点的父路径上所有节点的fiber.childLanes保证消费节点可以得到更新。围绕这两步还有两个关键支撑机制栈式状态管理beginWork阶段pushProvider入栈并更新context._currentValuecompleteWork阶段popProvider出栈恢复旧值借助深度优先遍历天然匹配栈的 push/pop 特性详见 React 算法之栈操作。更新传播与强制更新propagateContextChange通过dependencies链表精准定位 consumerscheduleWorkOnParentPath向上标记childLanes对 class 类型 consumer 额外注入ForceUpdate更新对象确保其绕过shouldComponentUpdate一定重新渲染。整个Context机制贯穿fiber树构造的创建与更新两条链路与 fiber 树构造(初次创建)、fiber 树构造(对比更新) 中的bailout、lanes机制紧密耦合理解了这一闭环也就真正掌握了 React Context 的底层工作原理。赞分享教程前端【免费下载链接】react-illustration-series图解react源码, 用大量配图的方式, 致力于将react原理表述清楚.项目地址https://gitcode.com/gh_mirrors/re/react-illustration-series点击查看免费下载相关推荐React Context上下文原理Provider与Consumer实现全解析React Context上下文原理Provider与Consumer实现全解析 引言你还在为组件通信烦恼吗 在React开发中组件间的数据传递一直前端UI组件nunchaku-flux.1-krea-dev实战教程集成Diffusers和ComfyUI的完整指南nunchaku flux.1 krea dev实战教程集成Diffusers和ComfyUI的完整指南 nunchaku flux.1 krea dev是一深度学习存量 RAG 项目补上多模态短板RAG-Anything 接入 LightRAG 实战存量 RAG 项目补上多模态短板RAG Anything 接入 LightRAG 实战 如果你的 RAG 服务已经基于 LightRAG 稳定运行现在想让它人工智能RAG多模态上一篇CoffeeScript 1.12.5 深度解析import/export 关键字处理改进与八进制转义校验修复下一篇攻克Eureka表单测试难关UI Testing自动化脚本实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表