React Context原理深入理解源码示例分析

目录
  • 正文
  • 一、概念
  • 二、使用
    • 2.1、React.createContext
    • 2.2、Context.Provider
    • 2.3、React.useContext
    • 2.4、Example
  • 三、原理分析
    • 3.1、createContext 函数实现
    • 3.2、 JSX 编译
    • 3.3、消费组件 - useContext 函数实现
    • 3.4、Context.Provider 在 Fiber 架构下的实现机制
    • 3.5、小结
  • 四、注意事项
  • 五、对比 useSelector

正文

在 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 的实现由三部分组成:

  • 创建 Context:React.createContext() 方法;
  • Provider 组件:<Context.Provider value={value}>
  • 消费 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 元素对象。

对象的 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 消费组件的分析,我们需要思考两点:

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

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

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

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 前后是否发生变化)。

  • 提供给 Context.Provider 的 value 对象地址不会发生变化,这使得子组件中使用了 useSelector -> useContext,但不会因顶层数据而进行重渲染。
  • store.state 数据变化组件如何更新呢?react-redux 订阅了 redux store.state 发生更新的动作,然后通知组件「按需」执行重渲染。

以上就是React Context原理深入理解源码示例分析的详细内容,更多关于React Context原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • React classnames原理及测试用例

    目录 前言 classnames 的用法 学会 classnames 的原理 测试用例的使用 总结 前言 本期的源码阅读任务是: 学会 classnames 的用法 学会 classnames 的原理 测试用例的使用 源码地址:JedWatson/classnames: A simple javascript utility for conditionally joining classNames together (github.com) classnames 的用法 Classname 是一

  • React redux 原理及使用详解

    目录 概述 createStore创建store applyMiddleware 应用中间件 combineReducers 合并多个reducer dispatch 中间件 中间件的调用顺序 store redux 数据流 bindActionCreators compose enhancer 使用 redux 常遇见的问题 概述 一个状态管理工具 Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个 Store. State:包含所有数据,如果想得到某个时点的数据,就要对

  • React不使用requestIdleCallback实现调度原理解析

    目录 1.起因 2.查找问题 3.解决问题 4.总结 5.吐槽 1.起因 最近在一边啃源码,一边手写fiber嘛,然后也看了很多博客和资料,基本上大伙好像都是说用requestIdleCallback来模拟react实现一个空闲时间调度.但我自己手写的时候把怎么用怎么怪,老是感觉有什么地方不对劲而且是在调度过程中,可能是因为我是想写出来来一个相对健全一点的模版方便我以后写源码的其他部分把,然后分析了一下所以有了这篇博客. 2.查找问题 1.requestIdleCallback是利用帧之间空闲时

  • React Hydrate原理源码解析

    目录 引言 Demo ReactDOM.render ReactDOM.hydrate hydrate 过程 事件绑定 hydrate 源码剖析 beginWork HostRoot Fiber HostComponent HostText Fiber tryToClaimNextHydratableInstance completeUnitOfWork popHydrationState prepareToHydrateHostInstance prepareToHydrateHostText

  • React开发进阶redux saga使用原理详解

    目录 前言 redux的特点 分析原理 1. 自动执行Generator 2. 发布订阅模式 3. put, takeEvery, delay, call返回effect 总结 前言 工作中使用了redux-saga这个redux中间件,如果不明白内部原理使用起来会让人摸不着头脑,阅读源码后特意对其原理做下总结. redux的特点 一个标准.管理应用副作用的redux中间件 实现切面编程方式 声明式的编写方式 订阅发布的设计模式 优点: 把异步操作转移到单独 saga文件中,而不是糅杂在acti

  • React应用框架Dva数据流向原理总结分析

    目录 Dva是什么? 流程图怎么看? Model对象属性 数据流向 Dva是什么? 在刚刚接触Dva时,我最想知道的第一个问题就是: Dva官网文档的介绍是: dva 是体验技术部开发的 React 应用框架,将上面三个 React 工具库包装在一起,简化了 API,让开发 React 应用更加方便和快捷. dva = React-Router + Redux + Redux-saga 说实话这些名词让我只能一个一个的百度,虽然不能说毫无收获,但是依然难以理解. 现在我的理解Dva是一个轻量化的

  • React Streaming SSR原理示例深入解析

    目录 功能简介 基本原理 使用示例 Streaming HTML Selective Hydration 降级逻辑 JS 和 CSS 设置 源码解析 数据结构 Segment Boundary Task Request 主要流程 功能简介 React 18 提供了一种新的 SSR 渲染模式: Streaming SSR.通过 Streaming SSR,我们可以实现以下两个功能: Streaming HTML:服务端可以分段传输 HTML 到浏览器,而不是像 React 18 以前一样,需要等待

  • React Context原理深入理解源码示例分析

    目录 正文 一.概念 二.使用 2.1.React.createContext 2.2.Context.Provider 2.3.React.useContext 2.4.Example 三.原理分析 3.1.createContext 函数实现 3.2. JSX 编译 3.3.消费组件 - useContext 函数实现 3.4.Context.Provider 在 Fiber 架构下的实现机制 3.5.小结 四.注意事项 五.对比 useSelector 正文 在 React 中提供了一种「

  • React Refs 的使用forwardRef 源码示例解析

    目录 三种使用方式 1. String Refs 2. 回调 Refs 3. createRef 两种使用目的 Refs 转发 createRef 源码 forwardRef 源码 三种使用方式 React 提供了 Refs,帮助我们访问 DOM 节点或在 render 方法中创建的 React 元素. React 提供了三种使用 Ref 的方式: 1. String Refs class App extends React.Component { constructor(props) { su

  • jQuery.prototype.init选择器构造函数源码思路分析

    一.源码思路分析总结 概要: jQuery的核心思想可以简单概括为"查询和操作dom",今天主要是分析一下jQuery.prototype.init选择器构造函数,处理选择器函数中的参数: 这个函数的参数就是jQuery()===$()执行函数中的参数,可以先看我之前写的浅析jQuery基础框架一文,了解基础框架后,再看此文. 思路分析: 以下是几种jQuery的使用情况(用于查询dom),每种情况都返回一个选择器实例(习惯称jQuery对象(一个nodeList对象),该对象包含查询

  • OpenMP task construct 实现原理及源码示例解析

    目录 前言 从编译器角度看 task construct Task Construct 源码分析 总结 前言 在本篇文章当中主要给大家介绍在 OpenMP 当中 task 的实现原理,以及他调用的相关的库函数的具体实现. 在本篇文章当中最重要的就是理解整个 OpenMP 的运行机制. 从编译器角度看 task construct 在本小节当中主要给大家分析一下编译器将 openmp 的 task construct 编译成什么样子,下面是一个 OpenMP 的 task 程序例子: #inclu

  • MyBatis SqlSource源码示例解析

    目录 正文 SqlNode SqlNode接口定义 BoundSql SqlSource SqlSource解析时机 SqlSource调用时机 总结 正文 MyBatis版本:3.5.12. 本篇讲从mybatis的角度分析SqlSource.在xml中sql可能是带?的预处理语句,也可能是带$或者动态标签的动态语句,也可能是这两者的混合语句. SqlSource设计的目标就是封装xml的crud节点,使得mybatis运行过程中可以直接通过SqlSource获取xml节点中解析后的SQL.

  • React实时预览react-live源码解析

    目录 引言 源码解读 输入内容 Provider generateElement 其他组件 总结 引言 react-live 是一个 react 的实时编辑器,可直接编辑 react 代码,并实时预览.可以看下官方的预览图: 本文针对的源码版本 src ├── components │ ├── Editor │ │ └── index.js │ └── Live │ ├── LiveContext.js │ ├── LiveEditor.js │ ├── LiveError.js │ ├── L

  • React实现合成事件的源码分析

    目录 事件绑定 事件触发 结尾 今天尝试学习 React 事件的源码实现. React 版本为 18.2.0 React 中的事件,是对原生事件的封装,叫做合成事件.抽象出一层合成事件,是为了做兼容,抹平不同浏览器之间的差异. 下面会从两个方面进行源码的解读: 事件绑定 事件触发 事件绑定 首先是 React 项目过程启动时,调用 listenToAllSupportedEvents 方法,做合成事件的绑定. // 对应方法 `ReactDOM.createRoot() function cre

  • Flink 侧流输出源码示例解析

    目录 Flink 侧流输出源码解析 源码解析 TimestampedCollector#collect CountingOutput#collect BroadcastingOutputCollector#collect RecordWriterOutput#collect ProcessOperator#ContextImpl#output CountingOutput#collect BroadcastingOutputCollector#collect RecordWriterOutput

  • React深入分析更新的创建源码

    目录 ReactDom.render setState 与 forceUpdate expirationTime的作用 获取currentTime 不同的expirationTime React 的鲜活生命起源于 ReactDOM.render ,这个过程会为它的一生储备好很多必需品,我们顺着这个线索,一探婴儿般 React 应用诞生之初的悦然. 更新创建的操作我们总结为以下两种场景 ReactDOM.render setState forceUpdate ReactDom.render 串联该

  • Flutter加载图片流程之ImageProvider源码示例解析

    目录 加载网络图片 ImageProvider resolve obtainKey resolveStreamForKey loadBuffer load(被废弃) evict 总结 困惑解答 加载网络图片 Image.network()是Flutter提供的一种从网络上加载图片的方法,它可以从指定的URL加载图片,并在加载完成后将其显示在应用程序中.本节内容,我们从源码出发,探讨下图片的加载流程. ImageProvider ImageProvider是Flutter中一个抽象类,它定义了一种

随机推荐