react context原理

news2025/7/19 12:25:20

带着问题思考:

  • 1 Provder 如何传递 context?
  • 2 三种获取 context 原理 ( Consumer, useContext,contextType )?
  • 3 消费 context 的组件,context 改变,为什么会订阅更新 (如何实现) 。
  • 4 context 更新,如何避免 pureComponent , shouldComponentUpdate 渲染控制策略的影响。
  • 5 如何实现的 context 嵌套传递 ( 多个 Povider )?

context对象

首先context对象是通过React.createContext创建的,我们看看这个api
在这里插入图片描述

  • 返回的就是一个对象,上面有Provider属性,本质其实就 Provider的elemetn类型。
  • 而Consumer更是直接指向本身,本质就是一个Context的element类型。
  • 上面还有其他的属性,比如_currentValue就是用来存放context的value的。

Provider

上面我们知道了provider就是一个特殊的react Element类型。那么我们重点看下Provider的实现原理。
围绕着两个点

  • Provider如何传递context状态。
  • Provider中value改变,如何订阅通知context。

首先看下demo
在这里插入图片描述
然后看看App执行beginWork,为provider这个儿子创建fiber时候的场景。

在这里插入图片描述

  • 这个就是Provider组件的vdom,本质也是一个element,只不过type上面有特殊的标识,标识这是一个Provider组件,并且可以通过type._context获取到value值。
  • 最终通过createFiberFromTypeAndProps为provider组件创建fiber的时候,
    在这里插入图片描述
    传入的type是一个对象。在这里插入图片描述
    最终走到这里逻辑,所以他的fiberTag就是ContextProvider在这里插入图片描述
    接着调用createFiber,返回fiber。此时Provider fiber大概长这样
{
alternate: null
elementType: {
"$$typeof": Symbol("react.provider")
_context: {}
}
firstEffect: null
lanes: 1
memoizedProps: null
mode: 8
nextEffect: null
pendingProps: {
children: {...}
value: {test: 1}
}
ref: null
return: null

sibling: null
stateNode: null
tag: ContextProvider

type: {
"$$typeof": Symbol("react.provider")
_context: {}
}
updateQueue: null
}

现在我们知道了Provider fiber长啥样了,现在看看当Provider fiber进入beginWork的时候,会走什么逻辑?
在这里插入图片描述
对于ContextProvider,会执行updateContextProvider
在这里插入图片描述
大概三个步骤:

  • 1 调用pushProvider,他会获取fiber上面的type,获取_context对象,将value赋值到context._currentValue属性上面。Provider就是通过这个来挂在value,即将value挂载到context._currentValue上面。
  • 2 判断新老props的value是否改变,(浅比较),如果改变调用propagateContextChange函数,没改变则停止调和子节点。
  • 3 如果改变,继续向下调和子节点。

重点看看propagateContextChange函数,它最终会调用propagateContextChange_eager函数

propagateContextChange_eager

简化后的函数

function propagateContextChange_eager(workInprogress, contextn, renderLanes){
let fiber = workInProgress.child;
 while (fiber !== null) {
    let nextFiber;
     // Visit this fiber.  每个context存放在fiber.dependencies上面
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        // 遍历所有context,因为context可能有多个
        // 如果该context是当前变化的context
        if (dependency.context === context) {
          // 如果是类组件
          if (fiber.tag === ClassComponent) {
            const lane = pickArbitraryLane(renderLanes);
            const update = createUpdate(NoTimestamp, lane);
            update.tag = ForceUpdate;
            // 创建Update,并且将他标记为forceUpdate
          

            // 插入fiber.updateQueue钟
            const updateQueue = fiber.updateQueue;
            if (updateQueue === null) {
              // Only occurs if the fiber has been unmounted.
            } else {
              const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
              const pending = sharedQueue.pending;
              if (pending === null) {
                // This is the first update. Create a circular list.
                update.next = update;
              } else {
                update.next = pending.next;
                pending.next = update;
              }
              sharedQueue.pending = update;
            }
          }

          // 将当前子节点的fiber的优先级更新
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          // 向上遍历父级fiber的childLanes
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress,
          );

          // Mark the updated lanes on the list, too.
          list.lanes = mergeLanes(list.lanes, renderLanes);

          // Since we already found a match, we can stop traversing the
          // dependency list.
          break;
        }
        dependency = dependency.next;
      }
      ...}


 if (nextFiber !== null) {
      nextFiber.return = fiber;
      // 如果nextFiber为null,表示没有子节点,那么就得处理兄弟节点,比如
      // <Provider><Son1/><Son2/></Provider> son1处理之后就得处理son2
    } else {
      // No child. Traverse to next sibling.
      nextFiber = fiber;
      while (nextFiber !== null) {
        if (nextFiber === workInProgress) {
          // We're back to the root of this subtree. Exit.
          nextFiber = null;
          break;
        }
        const sibling = nextFiber.sibling;
        if (sibling !== null) {
          // Set the return pointer of the sibling to the work-in-progress fiber.
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }
        // No more siblings. Traverse up.
        nextFiber = nextFiber.return;
      }
    }
    // 遍历条件
    fiber = nextFiber;
}
  • 首先,从ProviderFiber.child开始,通过while循环遍历,从每个fiber.dependencies获取一个数组,里面存放着所有的context,因为context可能有多个。
  • 遍历dependencies链表,如果当前context等于变化的context的时候,如果是类组件的话,当前创建一个update,并且将update的类型标记为forceUpdate(类似于调用this.forceUpdate带来的更新)。
  • 更新自身fiber和父级以上的fiber的优先级。
  • 继续遍历,如果没有儿子,那就遍历兄弟的,因为都属于Provider下面的节点。

这里可以罗列几个问题

fiber.dependencies是什么?

首先,fiber.dependencies存放着每个context,一个fiber可以有多个context与之对应,什么情况下会使用context呢?
1 有contextType静态属性指向的类组件
2 使用useContext的函数组件
3 使用了contet提供的Consumer
这里就可以推测,遇到上述3种的fiber,就会将context放入dependencies种。

为什么只针对类组件创建一个forceUpdate的update呢?

类组件如果要强制更新,就得通过PureComponent和shouldComponent等阻碍。而context要想突破这些限制,就比如做到当value改变,直接强制消费context的类组件更新,那么就需要通过forceUpdate了。
而这也解释了最开始的问题?context 更新,如何避免 pureComponent , shouldComponentUpdate 渲染控制策略的影响。,就是通过value改变,对于Provider下面的儿孙子们,只要有一个消费了context的类组件,直接创建一个forceUpdate的update。
在这里插入图片描述

为什么要遍历父级更新fiber的优先级。

react更新机制的原因,如果此次更新可能发生在fiber树上某一叶子种,因为context穿透影响,react并不知道此次更新的波及范围。那么如何处理呢?其实跟setState触发更新react重新更新的机制是一样的。

  • react会从RootFiber开始更新,每一个更新fiber都走一次beginWork,然后通过判断当前fiber.childLandes或者fiber.lanes是否等于此次更新的lane,以此来判断当前节点是否需要更新。
  • 有三种情况,
    1 如果遇到组件,但是fiber.childLanes和lanes都不等于,也就是说当前更新不涉及到该fiber所在的链上,那么就不会render,也不会向下beginWork。
    2 如果遇到组件,fiber.lanes不等于,但是fiber.childLanes等于,也就是说当前更新属于该节点下面的某个节点。那么就不render,但是会向下beginWork,目的明显,就是为了找到对应的更新组件。
    3 如果遇到hostComoonent如div,那么直接判断childLanes,如果不等于就退出,等于的话就继续向下beginWork。

以此我们就知道了,为什么当前fiber context变化的话,需要更新从该ifber到rootFiber上所有fiber的优先级,为的就是方便react更新的时候能顺利找到发生变化的fiber。
总结:
1 如果一个组件发生更新,那么当前组件到 fiber root 上的父级链上的所有 fiber ,更新优先级都会升高,都会触发 beginwork 。
2 render 不等于 beginWork,但是 render 发生,一定触发了 beginwork 。
3 一次 beginwork ,一个 fiber 下的同级兄弟 fiber 会发生对比,找到任务优先级高的 fiber 。向下 beginwork 。

Context更新的原理

只要Porvider上面的context发生变化,就会递归所有的子组件,只要是消费了context的fiber,都会给一个高优先级,并且向上更新父级fiber的优先级,然后react从rootFiber往下遍历,直到找到该fiber,进行更新。图所示:

发现context变化,类组件消费Context,提高优先级
在这里插入图片描述
react从rootFIber往下遍历,找到变化的fiber。
在这里插入图片描述

Consumer原理

上述说到了,Consumer其实就是context对象本身,而context对象本身就是一个element镀锡,类型为React_CONTEXT_TYPE。那么看看该对象作为组建的话,在beginWork的操作。
对于COnsumer组件,
在这里插入图片描述
他的fiberTag就是ContextConsumer,对应的在beginWork阶段调用的函数就是
在这里插入图片描述

function updateContextConsumer(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  let context: ReactContext<any> = workInProgress.type; //获取context对象
  context = (context: any)._context; //获取context对象
  const newProps = workInProgress.pendingProps; //即将更新的props
  const render = newProps.children; //得到render , consumer的children就是一个函数,参数就是value

   /* 读取 context */ 
  prepareToReadContext(workInProgress, renderLanes); 
  // 通过context对象获取到最新的value
  const newValue = readContext(context);

  let newChildren;
    // 将新的context通过props传给render,得到最新的vdom
    newChildren = render(newValue);

  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork; //打上标记

  // 开始根据新的子vdom调和子fiber
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  // 返回儿子
  return workInProgress.child;
}

如上所示,其实做的事情就是

  • 获取context,调用readContext获取最新的value。
  • 然后通过render(value)将value作为参数传入,因为Consumer的children就是一个函数。得到children
  • 继续调和children。

上面说到fiber是如何与context建立关联的,其实就是通过readContext。
在这里插入图片描述
如上,创建一个contextItem,多个contextItem通过链表关联。然后存放在fiber.dependencies上面,以此达到fiber和contex之间的联系。这样下次Provider更新的时候,才能遍历子孙组件,通过对比子孙组建的dependencies上面的context,找到需要更新的fiber,进行更新。

看完了Conusmer的原理,我们需要再了解一下contextType和useContext的原理。
其实也很简单

useContext

在这里插入图片描述
在保存hooks的对象中我们可以看到,useContext就是readContext,我们需要显示的传入context对象,才能获取value,并且readContext也会将该函数组件的fiber.dependencies和当前context建立关联,只要value改变,Provdier组件进行beginWork的时候,也能找到该函数组件进行标记,促使其渲染。

contextType原理

其原理和useContext一样,本质也是调用readContet在这里插入图片描述
类组件创建实例的时候,如果遇到了有静态属性contextType的,就直接调用readContext,然后也会将该类组件建立与当前context的关系,方便更新。

Provider 嵌套传递原理。

知道了ifber怎么存放context对象之后,多个Provider嵌套的原理其实也明白了。多个provider嵌套的话,如果有订阅的,就会建立关联,多个context对象同时共存于fiber.dependience,然后该怎么更新就怎么更新。因为每个context跟fiber的关联逻辑就在那。并不影响。

Context流程总结

  • 首先看createContext函数,返回了一个context对象,value值存放在了_currentValue上,而还提供了Provider对象和Consumer对象,本质也是react elemetn对象。

  • 其次对于Provider组件,每次进行beginWork的时候,都会判断当前的value是否改变,如果改变了,那么他会遍历所有的子孙fiber,如果遇到了消费context的fiber(通过获取fiber.dependencies上的contextItem对象),如果是类组件,那么直接创建一个forceUpdate的update,为的就是避免PureComoent和shouldComponentUpdate的影响。如果是其他组件,比如函数组件,或者是Consumer组件,就会提升其优先级,并且将fiber到rootFiber所有的fiber的chilLanes也提升优先级。

  • 对于Context的订阅一共有三种,useContext, Consumer, contextType,本质上都是调用readContext来建立fiber与context的关联(context对象通过链表存放在fiber.dependience上面),然后返回最新的value。

  • 只有订阅了context的fiber,才会建立关联,那么value改变的时候,Provider组件进行beginWork的时候才能找到订阅了context的fiber,而对其他没有订阅的fiber不会影响。

  • 文章通过学习掘金的《react进阶实践指南》作为笔记产出,文中图片部分来自《react进阶实践指南》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/37983.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue+iview将表格table以excel文件导出的几种方式

前言 在日常工作中&#xff0c;若是经常跟后台管理系统打交道的朋友想必对导出excel表格这种需求肯定很熟悉吧。不过我也问了身边的一些岗位为后端工程师的朋友&#xff0c;他们说在公司的话一般导出excel表格的工作一般由后端来做&#xff0c;前端只需要调用接口即可。我的话…

AI面试必备-《家居必备的AI精选资源列表》免费分享

本资源介绍 DeepMind科学家和工程师为有兴趣了解更多人工智能、机器学习和其他相关主题的学生整理的教育资源列表。 文末附本书免费获取地址。 内容截图 本资源免费下载地址 链接: https://pan.baidu.com/s/1IkPk0a3q2Z1z4FATG2y7HA?pwdwy3c 提取码: wy3c 往期精品内容推荐 大…

认知电子战 | 无线电中的认知理论

认知的概念 认知(Cognition)是指人认识外界事务的过程 认知本来是用于描述具有生命特征的物种的,借用于机器或系统上,就是指将认知的思想应用于机器上。 生物的认知特点: 感觉思考、推理、问题解答判断记忆分别对应于系统认知特点: 感知(各种传感器)机器学习算法、基…

算法提升:图的启发式搜索算法(A算法、A*算法)

启发式搜索算法 目录 概念 A算法 A*算法 概念 启发式搜索(Heuristically Search)又称为有信息搜索(Informed Search)&#xff0c;它是利用问题拥有的启发信息来引导搜索&#xff0c;达到减少搜索范围、降低问题复杂度的目的&#xff0c;这种利用启发信息的搜索过程称为启发…

修改 Git 已经提交记录的 用户名 和邮箱

修改 Git 已经提交记录的 用户名 和邮箱 有关 Git 和版本控制的常见问题。 如何更改提交的作者姓名/电子邮件&#xff1f; 在我们进入解决方案之前&#xff0c;让我们找出您到底想要完成什么&#xff1a; 在提交之前更改作者信息在提交后更改作者信息&#xff08;即历史提交…

常见的前端安全问题(xss / csrf / sql / ddos / cdn...)

目录 1. xss&#xff08;Cross Site Scripting&#xff09;跨站脚本攻击 1.1 持久型&#xff08;存储型&#xff09;攻击 / 非持久型&#xff08;反射型&#xff09;攻击 是什么&#xff1f; 1.2 xss 出现的场景&#xff1f;造成的危害&#xff1f; 1.3 防御 xss&#xff0…

three.js之Geometry顶点、颜色数据与三角面

文章目录简介顶点对于代码的解释颜色对于代码的解释三角面专栏目录请点击 简介 Geometry与BufferGeometry表达的含义相同&#xff0c;只是对象的结构不同three.js渲染的时候会先把Geometry转化为BufferGeometry在解析几何体顶点数据进行渲染 顶点 <!DOCTYPE html> <…

基于JavaWeb的药品进销存管理系统(JSP)

目 录 绪论 1 1.1 本课题的研究背景 1 1.2 国内外研究现状 1 1.3 本课题的主要工作 2 1.4 目的和意义 2 开发工具及技术 3 2.1 开发工具 3 2.1.1 MyEclipse 3 2.1.2 Tomcat 3 2.1.3 Mysql 3 2.2 开发技术 4 2.2.1 JSP 4 2.2.2 MyBatis 4 2.2.3 JavaScript 4 2.2.4 jQuery以及j…

机械原理-试题及答案

模拟试题八&#xff08;机械原理A&#xff09; 一、判断题&#xff08;10分&#xff09;[对者画√&#xff0c;错者画 ] 1、对心曲柄滑块机构都具有急回特性。&#xff08; &#xff09; 2、渐开线直齿圆柱齿轮的分度圆与节圆相等。&#xff08; &#xff09; 3、当两直齿圆柱齿…

Spring Cloud OpenFeign - 日志配置

项目源码地址&#xff1a;https://download.csdn.net/download/weixin_42950079/87168704 OpenFeign 有 4 种日志级别&#xff1a; NONE: 不记录任何日志&#xff0c;是OpenFeign默认日志级别&#xff08;性能最佳&#xff0c;适用于生产环境&#xff09;。BASIC: 仅记录请求方…

[附源码]计算机毕业设计JAVA人力资源管理系统论文2022

[附源码]计算机毕业设计JAVA人力资源管理系统论文2022 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM…

QT对象树和菜单操作

前言 可以与MFC框架进行比较&#xff0c;总结彼此的相同点和不同点&#xff0c;在此基础上再去学习其他的界面框架&#xff0c;达到增量学习的境界。 一.对象树 优点&#xff1a;当父对象被析构时&#xff0c;如果子对象没有被析构&#xff0c;QT的对象树机制会去析构它&…

pandas索引操作、赋值操作、排序以及Series排序和DataFrame排序

一、pandas索引操作 索引操作&#xff0c;使用索引选取序列和切片选择数据&#xff0c;也可以直接使用列名、行名称&#xff0c;或组合使用 直接使用行列索引&#xff1a;行列索引名顺序为先列再行&#xff0c;使用指定行列索引名&#xff0c;不能使用下标loc[行索引名&#…

MySQL数据库表空间回收问题

MySQL数据库表空间回收问题1. MySQL表空间回收2. MySQL表空间设置3. MySQL删除数据流程4. MySQL数据页空洞问题1. MySQL表空间回收 我们经常会发现一个问题&#xff0c;就是把表数据删除以后发现&#xff0c;数据文件大小并没有变化&#xff0c;这就是标题中所说的MySQL数据库…

Numpy数组中的维度和轴

维度究竟是行数还是列数&#xff1f; m维行向量&#xff1a;m维表示一行中有m列&#xff0c;由于是行向量&#xff0c;所以是1行m列n维列向量&#xff1a;n维表示一行中有n行&#xff0c;由于是列向量&#xff0c;所以是n行1列m维向量&#xff1a;看书的习惯了&#xff0c;一般…

APS生产计划排产 — 排产结果拉动模具工装需求计划

APS生产计划排产系统&#xff0c;对所有资源具有同步的&#xff0c;实时的&#xff0c;具有约束能力的&#xff0c;模拟能力&#xff0c;不论是物料&#xff0c;机器设备&#xff0c;人员&#xff0c;供应&#xff0c;客户需求&#xff0c;运输等影响计划因素。不论是长期的或短…

【CNN-SVM回归预测】基于CNN-SVM实现数据回归预测附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

JavaWeb开发之——多表查询(21)

一 概述 多表查询—简介多表查询—内连接&外连接多表查询—子查询 二 多表查询—简介 2.1 概念 多表查询顾名思义就是从多张表中一次性的查询出我们想要的数据。 2.2 SQL数据准备 DROP TABLE IF EXISTS emp; DROP TABLE IF EXISTS dept; # 创建部门表 CREATE TABLE de…

Word2Vec 实践

Word2Vec 实践 gensim库使用 这里的Word2Vec借助 gensim 库实现&#xff0c;首先安装pip install gensim3.8.3 from gensim.models.word2vec import Word2Vecmodel Word2Vec(sentencesNone, size100, alpha0.025, window5, min_count5,max_vocab_sizeNone, sample1e-3, …

【Java第34期】:Bean的六种作用域

作者&#xff1a;有只小猪飞走啦 博客地址&#xff1a;https://blog.csdn.net/m0_62262008?typeblog 内容&#xff1a;介绍Bean的六种作用域的效果以及适用场景 文章目录前言一&#xff0c;作用域定义以及Bean的六种作用域是什么&#xff1f;二&#xff0c;singleton&#x…