react源码分析:深度理解React.Context

news2024/9/28 1:25:13

开篇

在 React 中提供了一种「数据管理」机制:React.context,大家可能对它比较陌生,日常开发直接使用它的场景也并不多。

但提起 react-redux 通过 Providerstore 中的全局状态在顶层组件向下传递,大家都不陌生,它就是基于 React 所提供的 context 特性实现。

本文,将从概念、使用,再到原理分析,来理解 Context 在多级组件之间进行数据传递的机制。

一、概念

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

通常,数据是通过 props 属性自上而下(由父到子)进行传递,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

二、使用

下面我们以 Hooks 函数组件为例,展开介绍 Context 的使用。

2.1、React.createContext

首先,我们需要创建一个 React Context 对象。

const Context = React.createContext(defaultValue);

当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中的 Context.Provider 中读取到当前的 context.value 值。

当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。

2.2、Context.Provider

每个 Context 对象都会返回一个 Provider React 组件,它接收一个 value 属性,可将数据向下传递给消费组件。当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。

注意,当 value 传递为一个复杂对象时,若想要更新,必须赋予 value 一个新的对象引用地址,直接修改对象属性不会触发消费组件的重渲染。

<Context.Provider value={/* 某个值,一般会传递对象 */}>

2.3、React.useContext

Context Provider 组件提供了向下传递的 value 数据,对于函数组件,可通过 useContext API 拿到 Context value

const value = useContext(Context);

useContext 接收一个 context 对象(React.createContext 的返回值),返回该 context 的当前值。

当组件上层最近的 <Context.Provider> 更新时,当前组件会触发重渲染,并读取最新传递给 Context Provider 的 context value 值。

题外话:React.memo 只会针对 props 做优化,如果组件中 useContext 依赖的 context value 发生变化,组件依旧会进行重渲染。

2.4、Example

我们通过一个简单示例来熟悉上述 Context 的使用。

const Context = React.createContext(null);

const Child = () => {
  const value = React.useContext(Context);
  return (
    <div>theme: {value.theme}</div>
  )
}

const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <Context.Provider value={{ theme: 'light' }}>
      <div onClick={() => setCount(count + 1)}>触发更新</div>
      <Child />
    </Context.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'));

示例中,在 App 组件内使用 Providervalue 值向子树传递,Child 组件通过 useContext 读取 value,从而成为 Consumer 消费组件。

三、原理分析

从上面「使用」我们了解到:Context 的实现由三部分组成:

  1. 创建 Context:React.createContext() 方法;
  2. Provider 组件:<Context.Provider value={value}>
  3. 消费 value:React.useContext(Context) 方法。

原理分析脱离不了源码,下面我们挑选出核心代码来看看它们的实现。

3.1、createContext 函数实现

createContext 源码定义在 react/src/ReactContext.js 位置。它返回一个 context 对象,提供了 ProviderConsumer 两个组件属性,_currentValue 会保存 context.value 值。

const REACT_PROVIDER_TYPE = Symbol.for('react.provider');
const REACT_CONTEXT_TYPE = Symbol.for('react.context');

export function createContext<T>(defaultValue: T): ReactContext<T> {
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    // 并发渲染器方案,分为主渲染器和辅助渲染器
    _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;
}

尽管在这里我们只看到要返回一个对象,却看不出别的名堂,只需记住它返回的对象结构信息即可,我们接着往下看。相关参考视频讲解:进入学习

3.2、 JSX 编译

我们所编写的 JSX 语法在进入 render 时会被 babel 编译成 ReactElement 对象。我们可以在 babel repl 在线平台 转换查看。

JSX 语法最终会被转换成 React.createElement 方法,我们在 example 环境下执行方法,返回的结果是一个 ReactElement 元素对象。

截屏2022-08-29 下午8.46.48.png

对象的 props 保存了 context 要向下传递的 value,而对象的 type 则保存的是 context.Provider

context.Provider = {
  $$typeof: REACT_PROVIDER_TYPE,
  _context: context,
};

有了对象描述结构,接下来进入渲染流程并在 Reconciler/beginWork 阶段为其创建 Fiber 节点。

3.3、消费组件 - useContext 函数实现

在介绍 Provider Fiber 节点处理前,我们需要先了解下 Consumer 消费组件如何使用 context value,以便于更好理解 Provider 的实现。

useContext 接收 context 对象作为参数,从 context._currentValue 中读取 value 值。

不过,除了读取 value 值外,还会将 context 信息保存在当前组件 Fiber.dependencies 上。

目的是为了在 Provider value 发生更新时,可以查找到消费组件并标记上更新,执行组件的重渲染逻辑。

function useContext(Context) {
  // 将 context 记录在当前 Fiber.dependencies 节点上,在 Provider 检测到 value 更新后,会查找消费组件标记更新。
  const contextItem = {
    context: context,
    next: null, // 一个组件可能注册多个不同的 context
  };
  if (lastContextDependency === null) {
    lastContextDependency = contextItem;
    currentlyRenderingFiber.dependencies = {
      lanes: NoLanes,
      firstContext: contextItem,
      responders: null
    };
  } else {
    // Append a new context item.
    lastContextDependency = lastContextDependency.next = contextItem;
  }
  return context._currentValue;
}

3.4、Context.Provider 在 Fiber 架构下的实现机制

经过上面 useContext 消费组件的分析,我们需要思考两点:

  1. <Provider> 组件上的 value 值何时更新到 context._currentValue
  2. Provider.value 值发生更新后,如果能够让消费组件进行重渲染 ?

这两点都会在这里找到答案。

在 example 中,点击「触发更新」div 后,React 会进入调度更新阶段。我们通过断点定位到 Context.Provider Fiber 节点的 Reconciler/beginWork 之中。

截屏2022-08-30 下午9.15.10.png

Provider Fiber 类型为 ContextProvider,因此进入 tag switch case 中的 updateContextProvider

function beginWork(current, workInProgress, renderLanes) {
  ...
  switch (workInProgress.tag) {
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
  }
}

首先,更新 context._currentValue,比较新老 value 是否发生变化。

注意,这里使用的是 Object.is,通常我们传递的 value 都是一个复杂对象类型,它将比较两个对象的引用地址是否相同。

若引用地址未发生变化,则会进入 bailout 复用当前 Fiber 节点。

在 bailout 中,会检查该 Fiber 的所有子孙 Fiber 是否存在 lane 更新。若所有子孙 Fiber 本次都没有更新需要执行,则 bailout 会直接返回 null,整棵子树都被跳过更新。

function updateContextProvider(current, workInProgress, renderLanes) {
  var providerType = workInProgress.type;
  var context = providerType._context;
  var newProps = workInProgress.pendingProps;
  var oldProps = workInProgress.memoizedProps;
  var newValue = newProps.value;
  var oldValue = oldProps.value;

  // 1、更新 value prop 到 context 中
  context._currentValue = nextValue;

  // 2、比较前后 value 是否有变化,这里使用 Object.is 进行比较(对于对象,仅比较引用地址是否相同)
  if (objectIs(oldValue, newValue)) {
    // children 也相同,进入 bailout,结束子树的协调
    if (oldProps.children === newProps.children && !hasContextChanged()) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  } else {
    // 3、context value 发生变化,深度优先遍历查找 consumer 消费组件,标记更新
    propagateContextChange(workInProgress, context, changedBits, renderLanes);
  }

  // ... reconciler children
}

context.value 发生变化,调用 propagateContextChange 对 Fiber 子树向下深度优先遍历,目的是为了查找 Context 消费组件,并为其标记 lane 更新,即让其后续进入 Reconciler/beginWork 阶段后不满足 bailout 条件 !includesSomeLane(renderLanes, updateLanes)

function propagateContextChange(workInProgress, context, changedBits, renderLanes) {
  var fiber = workInProgress.child;

  while (fiber !== null) {
    var nextFiber;
    var list = fiber.dependencies; // 若 fiber 属于一个 Consumer 组件,dependencies 上记录了 context 对象

    if (list !== null) {
      var dependency = list.firstContext; // 拿出第一个 context
      while (dependency !== null) {
        // Check if the context matches.
        if (dependency.context === context) {
          if (fiber.tag === ClassComponent) {
            var update = createUpdate(NoTimestamp, pickArbitraryLane(renderLanes));
            update.tag = ForceUpdate;
            enqueueUpdate(fiber, update);
          }
          // 标记组件存在更新,!includesSomeLane(renderLanes, updateLanes) 
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          // 在上层 Fiber 树的节点上标记 childLanes 存在更新
          scheduleWorkOnParentPath(fiber.return, renderLanes);
          ...
          break
        }
      }
    }
  }
}

3.5、总结

通常,一个组件的更新可通过执行内部 setState 来生成,其方式也是标记 Fiber.lane 让组件不进入 bailout;

对于 Context,当 Provider.value 发生更新后,它会查找子树找到消费组件,为消费组件的 Fiber 节点标记 lane。

当组件(函数组件)进入 Reconciler/beginWork 阶段进行处理时,不满足 bailout,就会重新被调用进行重渲染,这时执行 useContext,就会拿到最新的 context.__currentValue

这就是 React.context 实现过程。

四、注意事项

React 性能一大关键在于,减少不必要的 render。Context 会通过 Object.is(),即 === 来比较前后 value 是否严格相等。这里可能会有一些陷阱:当注册 Provider 的父组件进行重渲染时,会导致消费组件触发意外渲染。

如下例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有消费组件,因为 value 属性总是被赋值为新的对象:

class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value={{something: 'something'}}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}

为了防止这种情况,可以将 value 状态提升到父节点的 state 里:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: { something: 'something' },
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

五、对比 useSelector

从「注意事项」可以考虑:要想使消费组件进行重渲染,context value 必须返回一个全新对象,这将导致所有消费组件都进行重渲染,这个开销是非常大的,因为有一些组件所依赖的值可能并未发生变化。

当然有一种直观做法是将「状态」分离在不同 Context 之中。

react-redux useSelector 则是采用订阅 redux store.state 更新,去通知消费组件「按需」进行重渲染(比较所依赖的 state 前后是否发生变化)。

  1. 提供给 Context.Provider 的 value 对象地址不会发生变化,这使得子组件中使用了 useSelector -> useContext,但不会因顶层数据而进行重渲染。

  2. store.state 数据变化组件如何更新呢?react-redux 订阅了 redux store.state 发生更新的动作,然后通知组件「按需」执行重渲染。

最后

感谢阅读,如有不足之处,欢迎指出讨论。

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

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

相关文章

第十期|惊!游戏广告主投放十万被骗,推广作弊竟全是虚拟用户

顶象防御云业务情报中心发现&#xff0c;在互联网生态中存在很多灰色的渠道刷量工作室&#xff0c;渠道方通常以低廉的价格通过这些工作室提高广告URL点击量、应用下载激活量、注册量和真实的推广数据提起反馈给广告主结算&#xff0c;但其提供的数据质量和价格一样低廉&#x…

用DevExpress实现基于HTMLCSS的桌面应用程序的UI(一)

DevExpress WinForm拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForm能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜任…

RabbitMQ的高可用和高可靠

01、什么是高可用机制 所谓高可用&#xff1a;是指产品在规定的条件和规定的时刻或时间内处于可执行规定功能状态的能力。 当业务量增加时&#xff0c;请求也过大&#xff0c;一台消息中间件服务器的会触及硬件&#xff08;CPU,内存&#xff0c;磁盘&#xff09;的极限&#x…

从输入url到页面展现的全过程

作为一道面试经常会问到的题目&#xff0c;看过很多写的很好的博文&#xff0c;整理成自己的笔记 大概来说分为以下几个过程&#xff1a; 输入url地址应用层进行DNS解析应用层生成HTTP请求报文传输层建立TCP连接网络层使用IP协议来选择路线数据链路层实现网络相邻节点间可靠的…

vue-pdf+element实现全屏窗口pdf分页预览,pdf打印实现和解决打印乱码

一.源码 vue-pdf打印实现和乱码解决https://download.csdn.net/download/lucky_fang/85498529 二.全屏窗口打印预览效果 分页预览pdf 窗口采用element窗口模板实现&#xff0c;样式可根据自己喜欢的效果进行修改 Element官方文档 终于可以打印测试了&#xff0c;乱码~&…

【优化求解】粒子群算法求解干扰受限无人机辅助网络优化问题【含Matlab源码 230期】

⛄一、粒子群简介 1 粒子群优化算法 粒子群优化算法( PSO)是指通过模拟鸟群觅食的协作行为,实现群体最优化。PSO是一种并行计算的智能算法,其基本模型如下: 假设群体规模为M,在D维空间中,群体中的第i个个体表示为XD ( xm1,xm2…xm D)T,速度表示为VD ( vm1,vm2…vm D)T,位置( …

聊一聊我对Restful理解

概念 REST原则提倡按照HTTP的语义使用HTTP&#xff0c;如果一个系统符合REST原则&#xff0c;我们就说这个系统是Restful风格的。Restful是Web API设计中非常重要的一个概念&#xff0c;但是很多开发人员对于Restful的理解存在误区。 什么是Restful 在说什么是Restful 之前&…

MES必懂知识,市场需求下的生产管理系统

任何事物的产生和发展都与市场的需求是分不开的&#xff0c;只有当市场需求新生的事物的时候&#xff0c;他才会兴起&#xff0c;有的事物早已经产生&#xff0c;在当时的环境下并未兴起&#xff0c;却在后来才兴盛&#xff0c;这是市场的需求的影响。 MES便是在市场需求下诞生…

高通导航器软件开发包使用指南(9)

高通导航器软件开发包使用指南&#xff08;9&#xff09;8参数说明8.1最小条件参数8.2光学流量数据图8.3光学流量估算数据图8.4光学流量估算图8.5 pos_hold_mode内存8.6体积参数8.7障碍物输出参数8.8速度平滑曲线8参数说明 每辆车必须在适当的位置有运行时参数文件&#xff0c…

How to build several ftp servers on one mac OS

1 How to establish one ftp server locally? I have viewed one article which helps us how to do this on mac OS. You can refer to this link Mac下搭建FTP服务器 please read the tutorial seriously. What I do in the following will be based on it. 2 How to buil…

centos7操作系统开机提示error:file “/boot/grub/i386-pc/normal.mod“ not found

一.问题现象 公司重要业务虚拟机突然业务访问不了&#xff0c;重启操作系统之后发现操作系统启动不了&#xff0c;直接进入救援模式&#xff0c;提示error:file “/boot/grub/i386-pc/normal.mod“ not found&#xff0c;报错截图 二.处理思路 在救援模式下&#xff0c;只有很…

用HTML+CSS做一个简单好看的汽车网页

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

网页开发——淘宝首页导航

这篇博文主要是重新学习&#xff08;复习&#xff09;前端知识&#xff0c;通过写淘宝购物首页导航为案例。 html主要书写内容&#xff1a; 1.首先我写了一个大盒子&#xff0c;用于存放所用的全部标签 <div class"nav">主要内容 </div> 2.插入一张淘宝…

AE/PR模板:镜头光晕炫光动画叠加特效 Premium Overlays Light Leaks

Premium Overlays Light Leaks是一款镜头光晕炫光动画叠加特效的AE/PR模板&#xff0c;非常适用于人物传记、演示文稿、出游vlog、产品展示等场合使用&#xff0c;为您的作品增添更出彩的视觉效果&#xff0c;喜欢的朋友不要错过哦~ 适用软件&#xff1a; AE 2019 或更高, PR 2…

YUV图像格式转换方法实践

一 I420转NV12 1.I420格式 &#xff08;1&#xff09;I420是每四个Y共用一组UV&#xff0c;如果一帧I420图像宽带是width&#xff0c;高度是height&#xff0c;1个像素占1个字节&#xff0c;那么共有width✖height个Y&#xff0c;U和V都是width✖height / 4个&#xff0c;因此…

JS 数据结构:队列

队列 定义&#xff1a; 队列(Queue) 是一种只在表的一端进行插入&#xff0c;而在另一端进行删除的数据结构。 队头(front) 允许删除的一端&#xff0c;永远指向第一个元素前一个位置。队尾(rear) 允许插入的一端&#xff0c;永远是指向队列最后一个元素先进先出(First In Fi…

一个漏测Bug能让你想到多少?

一、背景 漏测Bug是指产品逻辑缺陷在测试过程中没有被发现&#xff08;尤其是测试环境可以重现的缺陷&#xff09;&#xff0c;上线版本发布后或者在用户使用体验后发现并反馈回来的缺陷。可能造成线上故障或者资损&#xff0c;在对产品测试过程中&#xff0c;自己也难免出现一…

大二Web课程设计期末考试——基于HTML+CSS+JavaScript+jQuery电商类化妆品购物商城

常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他等网页设计题目, A…

[C++] std::ranges中的特征和自定义std::ranges::view变换

文章目录1. std::ranges中的特征1.1. std::ranges::range例子细化1.2. std::ranges::sized_range1.3. std::ranges::borrowed_range1.4. std::ranges::view2. std::ranges::subrange 迭代器-哨位对2.1. 构造2.2. 结构化解绑2.3. 操作3. std::views中的std::ranges::view变换3.1…

如何安装 Elasticsearch

实战场景 如何安装 Elasticsearch 知识点 •CentOS •Java •Elasticsearch 安装 •Kibana 安装 菜鸟实战 Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎&#xff0c;基于 RESTful web 接口。Elasticsearch 是用 Java 语言开发的…