React Context 变迁及背后实现原理详解

目录
  • Context
  • 老的 Context API
    • 基础示例
    • context 中断问题
    • 解决方案
  • 新的 Context API
    • 基础示例
    • 模拟实现
    • createContext 源码

Context

本篇我们讲 Context,Context 可以实现跨组件传递数据,大部分的时候并无需要,但有的时候,比如用户设置 了 UI 主题、地区偏好,如果从顶层一层层往下传反而有些麻烦,不如直接借助 Context 实现数据传递。

老的 Context API

基础示例

在讲最新的 API 前,我们先回顾下老的 Context API:

class Child extends React.Component {
  render() {
    // 4. 这里使用 this.context.value 获取
    return <p>{this.context.value}</p>
  }
}
// 3. 子组件添加 contextTypes 静态属性
Child.contextTypes = {
  value: PropTypes.string
};
class Parent extends React.Component {
  state = {
    value: 'foo'
  }
  // 1. 当 state 或者 props 改变的时候,getChildContext 函数就会被调用
  getChildContext() {
    return {value: this.state.value}
  }
  render() {
    return (
      <div>
        <Child />
      </div>
    )
  }
}
// 2. 父组件添加 childContextTypes 静态属性
Parent.childContextTypes = {
  value: PropTypes.string
};

context 中断问题

对于这个 API,React 官方并不建议使用,对于可能会出现的问题,React 文档给出的介绍为:

问题是,如果组件提供的一个 context 发生了变化,而中间父组件的 shouldComponentUpdate 返回 false,那么使用到该值的后代组件不会进行更新。使用了 context 的组件则完全失控,所以基本上没有办法能够可靠的更新 context。

对于这个问题,我们写个示例代码:

// 1. Child 组件使用 PureComponent
class Child extends React.Component {
  render() {
    return <GrandChild />
  }
}
class GrandChild extends React.Component {
  render() {
    return <p>{this.context.theme}</p>
  }
}
GrandChild.contextTypes = {
  theme: PropTypes.string
};
class Parent extends React.Component {
  state = {
    theme: 'red'
  }
  getChildContext() {
    return {theme: this.state.theme}
  }
  render() {
    return (
      <div onClick={() => {
        this.setState({
          theme: 'blue'
        })
      }}>
        <Child />
        <Child />
      </div>
    )
  }
}
Parent.childContextTypes = {
  theme: PropTypes.string
};

在这个示例代码中,当点击文字 red 的时候,文字并不会修改为 blue,如果我们把 Child 改为 extends Component,则能正常修改

这说明当中间组件的 shouldComponentUpdatetrue 时,会中断 Context 的传递。

PureComponent 的存在是为了减少不必要的渲染,但我们又想 Context 能正常传递,哪有办法可以解决吗?

既然 PureComponent 的存在导致了 Context 无法再更新,那就干脆不更新了,Context 不更新,GrandChild 就无法更新吗?

解决方案

方法当然是有的:

// 1. 建立一个订阅发布器,当然你也可以称呼它为依赖注入系统(dependency injection system),简称 DI
class Theme {
  constructor(value) {
    this.value = value
    this.subscriptions = []
  }
  setValue(value) {
    this.value = value
    this.subscriptions.forEach(f => f())
  }
  subscribe(f) {
    this.subscriptions.push(f)
  }
}
class Child extends React.PureComponent {
    render() {
        return <GrandChild />
    }
}
class GrandChild extends React.Component {
    componentDidMount() {
      // 4. GrandChild 获取 store 后,进行订阅
        this.context.theme.subscribe(() => this.forceUpdate())
    }
    // 5. GrandChild 从 store 中获取所需要的值
    render() {
        return <p>{this.context.theme.value}</p>
    }
}
GrandChild.contextTypes = {
  theme: PropTypes.object
};
class Parent extends React.Component {
    constructor(p, c) {
      super(p, c)
      // 2. 我们实例化一个 store(想想 redux 的 store),并存到实例属性中
      this.theme = new Theme('blue')
    }
    // 3. 通过 context 传递给 GrandChild 组件
    getChildContext() {
        return {theme: this.theme}
    }
    render() {
        // 6. 通过 store 进行发布
        return (
            <div onClick={() => {
                this.theme.setValue('red')
            }}>
              <Child />
              <Child />
            </div>
        )
    }
}
Parent.childContextTypes = {
  theme: PropTypes.object
};

为了管理我们的 theme ,我们建立了一个依赖注入系统(DI),并通过 Context 向下传递 store,需要用到 store 数据的组件进行订阅,传入一个 forceUpdate 函数,当 store 进行发布的时候,依赖 theme 的各个组件执行 forceUpdate,由此实现了在 Context 不更新的情况下实现了各个依赖组件的更新。

你可能也发现了,这有了一点 react-redux 的味道。

当然我们也可以借助 Mobx 来实现并简化代码,具体的实现可以参考 Michel Weststrate(Mobx 的作者) 的 How to safely use React context

新的 Context API

基础示例

想必大家都或多或少的用过,我们直接上示例代码:

// 1. 创建 Provider 和 Consumer
const {Provider, Consumer} = React.createContext('dark');
class Child extends React.Component {
  // 3. Consumer 组件接收一个函数作为子元素。这个函数接收当前的 context 值,并返回一个 React 节点。
  render() {
    return (
      <Consumer>
        {(theme) => (
        <button>
          {theme}
        </button>
      )}
      </Consumer>
    )
  }
}
class Parent extends React.Component {
  state = {
    theme: 'dark',
  };
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        theme: 'light'
      })
    }, 2000)
  }
  render() {
    // 2. 通过 Provider 的 value 传递值
    return (
      <Provider value={this.state.theme}>
        <Child />
      </Provider>
    )
  }
}

当 Provider 的 value 值发生变化时,它内部的所有 consumer 组件都会重新渲染。

新 API 的好处就在于从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。

模拟实现

那么 createContext 是怎么实现的呢?我们先不看源码,根据前面的订阅发布器的经验,我们自己其实就可以写出一个 createContext 来,我们写一个试试:

class Store {
    constructor() {
        this.subscriptions = []
    }
    publish(value) {
        this.subscriptions.forEach(f => f(value))
    }
    subscribe(f) {
        this.subscriptions.push(f)
    }
}
function createContext(defaultValue) {
    const store = new Store();
    // Provider
    class Provider extends React.PureComponent {
        componentDidUpdate() {
            store.publish(this.props.value);
        }
        componentDidMount() {
            store.publish(this.props.value);
        }
        render() {
            return this.props.children;
        }
    }
    // Consumer
    class Consumer extends React.PureComponent {
        constructor(props) {
            super(props);
            this.state = {
                value: defaultValue
            };
            store.subscribe(value => {
                this.setState({
                        value
                });
            });
        }
        render() {
            return this.props.children(this.state.value);
        }
    }
    return {
            Provider,
            Consumer
    };
}

用我们写的 createContext 替换 React.createContext 方法,你会发现,同样可以运行。

它其实跟解决老 Context API 问题的方法是一样的,只不过是做了一层封装。Consumer 组件构建的时候进行订阅,当 Provider 有更新的时候进行发布,这样就跳过了 PureComponent 的限制,实现 Consumer 组件的更新。

createContext 源码

现在我们去看看真的 createContext 源码,源码位置packages/react/src/ReactContext.js,简化后的代码如下:

import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
export function createContext(defaultValue) {
  const context = {
    $$typeof: REACT_CONTEXT_TYPE,
    // 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,
    // Used to track how many concurrent renderers this context currently
    // supports within in a single renderer. Such as parallel server rendering.
    _threadCount: 0,
    // These are circular
    Provider: null,
    Consumer: null,
    // Add these to use same hidden class in VM as ServerContext
    _defaultValue: null,
    _globalName: null,
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;
  return context;
}

你会发现,如同之前的文章中涉及的源码一样,React 的 createContext 就只是返回了一个数据对象,但没有关系,以后的文章中会慢慢解析实现过程。

React 系列

React 之 createElement 源码解读

React 之元素与组件的区别

React 之 Refs 的使用和 forwardRef 的源码解读

React 系列的预热系列,带大家从源码的角度深入理解 React 的

以上就是React Context 变迁及背后实现原理详解的详细内容,更多关于React Context 变迁原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • react中如何对自己的组件使用setFieldsValue

    目录 react对自己的组件使用setFieldsValue setFieldsValue的用法 问题 使用Hooks使用setFieldsValue设置初始值无效 总结 react对自己的组件使用setFieldsValue setFieldsValue的用法 setFieldsValue是antd form的一个api,其作用是对指定的已使用from包裹的表单进行value设置.那么所以它的功能也很简单,那就是给指定的input设置value. 如下所示: import React from

  • js中自定义react数据验证组件实例详解

    我们在做前端表单提交时,经常会遇到要对表单中的数据进行校验的问题.如果用户提交的数据不合法,例如格式不正确.非数字类型.超过最大长度.是否必填项.最大值和最小值等等,我们需要在相应的地方给出提示信息.如果用户修正了数据,我们还要将提示信息隐藏起来. 有一些现成的插件可以让你非常方便地实现这一功能,如果你使用的是knockout框架,那么你可以借助于Knockout-Validation这一插件.使用起来很简单,例如我下面的这一段代码: ko.validation.locale('zh-CN');

  • 浅析JS中什么是自定义react数据验证组件

    我们在做前端表单提交时,经常会遇到要对表单中的数据进行校验的问题.如果用户提交的数据不合法,例如格式不正确.非数字类型.超过最大长度.是否必填项.最大值和最小值等等,我们需要在相应的地方给出提示信息.如果用户修正了数据,我们还要将提示信息隐藏起来. 有一些现成的插件可以让你非常方便地实现这一功能,如果你使用的是knockout框架,那么你可以借助于Knockout-Validation这一插件.使用起来很简单,例如我下面的这一段代码: ko.validation.locale('zh-CN');

  • React为什么需要Scheduler调度器原理详解

    目录 正文 我们为什么需要Scheduler(调度器) Scheduler如何进行工作 总结 正文 最近在重学React,由于近两年没使用React突然重学发现一些很有意思的概念,首先便是React的Scheduler(调度器) 由于我对React的概念还停留在React 15之前(就是那个没有hooks的年代),所以接触Scheduler(调度器) 让我感觉很有意思: 在我印象中React的架构分为两层(React 16 之前) Reconciler(协调器)—— 负责找出变化的组件 Rend

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

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

  • React Context源码实现原理详解

    目录 什么是 Context Context 使用示例 createContext Context 的设计非常特别 useContext useContext 相关源码 debugger 查看调用栈 什么是 Context 目前来看 Context 是一个非常强大但是很多时候不会直接使用的 api.大多数项目不会直接使用 createContext 然后向下面传递数据,而是采用第三方库(react-redux). 想想项目中是不是经常会用到 @connect(...)(Comp) 以及 <Pro

  • TypeScript 背后的结构化类型系统原理详解

    目录 前言 什么是结构化类型系统? 什么是标称类型系统? 结构化类型系统等价于鸭子类型系统吗? 如何在 TypeScript 中模拟标称类型系统? 交叉类型实现 类实现 总结 前言 你能说清楚类型.类型系统.类型检查这三个的区别吗?在理解TypeScript的结构化类型系统之前,我们首先要搞清楚这三个概念和它们之间的关系 类型:即对变量的访问限制与赋值限制.如 TypeScript 中的原始类型.对象类型.函数类型和字面量类型等类型,当一个变量类型确定后,你不能访问这个类型中不存在的属性或方法,

  • Spring IOC原理详解

    最近,买了本Spring入门书:springInAction.大致浏览了下感觉还不错.就是入门了点.Manning的书还是不错的,我虽然不像哪些只看Manning书的人那样专注于Manning,但怀着崇敬的心情和激情通览了一遍.又一次接受了IOC.DI.AOP等Spring核心概念.先就IOC和DI谈一点我的看法. IOC(DI):其实这个Spring架构核心的概念没有这么复杂,更不像有些书上描述的那样晦涩.java程序员都知道:java程序中的每个业务逻辑至少需要两个或以上的对象来协作完成,通

  • Spring @Transactional工作原理详解

    本文将深入研究Spring的事务管理.主要介绍@Transactional在底层是如何工作的.之后的文章将介绍: propagation(事务传播)和isolation(隔离性)等属性的使用 事务使用的陷阱有哪些以及如何避免 JPA和事务管理 很重要的一点是JPA本身并不提供任何类型的声明式事务管理.如果在依赖注入容器之外使用JPA,事务处理必须由开发人员编程实现. UserTransaction utx = entityManager.getTransaction(); try{ utx.be

  • java进行远程部署与调试及原理详解

    这篇文章主要介绍了java进行远程部署与调试及原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 远程调试,特别是当你在本地开发的时候,你需要调试服务器上的程序时,远程调试就显得非常有用. JAVA 支持调试功能,本身提供了一个简单的调试工具JDB,支持设置断点及线程级的调试同时,不同的JVM通过接口的协议联系,本地的Java文件在远程JVM建立联系和通信.此篇是Intellij IDEA远程调试的教程汇总和原理解释,知其然而又知其所以然.

  • JavaScript原型继承和原型链原理详解

    这篇文章主要介绍了JavaScript原型继承和原型链原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在讨论原型继承之前,先回顾一下关于创建自定义类型的方式,这里推荐将构造函数和原型模式组合使用,通过构造函数来定义实例自己的属性,再通过原型来定义公共的方法和属性. 这样一来,每个实例都有自己的实例属性副本,又能共享同一个方法,这样的好处就是可以极大的节省内存空间.同时还可以向构造函数传递参数,十分的方便. 这里还要再讲一下两种特色的构造

  • Spring事务annotation原理详解

    这篇文章主要介绍了Spring事务annotation原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在使用Spring的时候,配置文件中我们经常看到 annotation-driven 这样的注解,其含义就是支持注解,一般根据前缀 tx.mvc 等也能很直白的理解出来分别的作用. <tx:annotation-driven/> 就是支持事务注解的(@Transactional) . <mvc:annotation-driven

  • SpringBoot自动装配原理详解

    首先对于一个SpringBoot工程来说,最明显的标志的就是 @SpringBootApplication它标记了这是一个SpringBoot工程,所以今天的 SpringBoot自动装配原理也就是从它开始说起. 自动装配流程 首先我们来看下@SpringBootApplication 这个注解的背后又有什么玄机呢,我们按下 ctrl + 鼠标左键,轻轻的点一下,此时见证奇迹的时刻.. 我们看到如下优雅的代码: 这其中有两个比较容易引起我们注意的地方,一个是@SpringBootConfigur

随机推荐