React 之 Context 的变迁与背后实现

简介: React 之 Context 的变迁与背后实现

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,则能正常修改


这说明当中间组件的 shouldComponentUpdatefalse 时,会中断  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 就只是返回了一个数据对象,但没有关系,以后的文章中会慢慢解析实现过程。

目录
相关文章
|
3月前
|
JavaScript 前端开发 API
[译] 用 Vue 3 Composition API 实现 React Context/Provider 模式
[译] 用 Vue 3 Composition API 实现 React Context/Provider 模式
|
2月前
|
前端开发
React属性之context属性
React中的Context属性用于跨组件传递数据,通过Provider和Consumer组件实现数据的提供和消费。
34 3
|
3月前
|
存储 JavaScript 前端开发
探索React状态管理:Redux的严格与功能、MobX的简洁与直观、Context API的原生与易用——详细对比及应用案例分析
【8月更文挑战第31天】在React开发中,状态管理对于构建大型应用至关重要。本文将探讨三种主流状态管理方案:Redux、MobX和Context API。Redux采用单一存储模型,提供预测性状态更新;MobX利用装饰器语法,使状态修改更直观;Context API则允许跨组件状态共享,无需第三方库。每种方案各具特色,适用于不同场景,选择合适的工具能让React应用更加高效有序。
72 0
|
3月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
34 0
|
3月前
|
开发者
告别繁琐代码,JSF标签库带你走进高效开发的新时代!
【8月更文挑战第31天】JSF(JavaServer Faces)标准标签库为页面开发提供了大量组件标签,如`&lt;h:inputText&gt;`、`&lt;h:dataTable&gt;`等,简化代码、提升效率并确保稳定性。本文通过示例展示如何使用这些标签实现常见功能,如创建登录表单和展示数据列表,帮助开发者更高效地进行Web应用开发。
42 0
|
3月前
|
容器 Kubernetes Docker
云原生JSF:在Kubernetes的星辰大海中,让JSF应用乘风破浪!
【8月更文挑战第31天】在本指南中,您将学会如何在Kubernetes上部署JavaServer Faces (JSF)应用,享受容器化带来的灵活性与可扩展性。文章详细介绍了从构建Docker镜像到配置Kubernetes部署全流程,涵盖Dockerfile编写、Kubernetes资源配置及应用验证。通过这些步骤,您的JSF应用将充分利用Kubernetes的优势,实现自动化管理和高效运行,开启Java Web开发的新篇章。
51 0
|
3月前
|
前端开发 JavaScript API
掌握React表单管理的高级技巧:探索Hooks和Context API如何协同工作以简化状态管理与组件通信
【8月更文挑战第31天】在React中管理复杂的表单状态曾是一大挑战,传统上我们可能会依赖如Redux等状态管理库。然而,React Hooks和Context API的引入提供了一种更简洁高效的解决方案。本文将详细介绍如何利用Hooks和Context API来优化React应用中的表单状态管理,通过自定义Hook `useForm` 和 `FormContext` 实现状态的轻松共享与更新,使代码更清晰且易于维护,为开发者带来更高效的开发体验。
44 0
|
3月前
|
前端开发 API 开发者
【React状态管理新思路】Context API入门:从零开始摆脱props钻孔的优雅之道,全面解析与实战案例分享!
【8月更文挑战第31天】React 的 Context API 有效解决了多级组件间状态传递的 &quot;props 钻孔&quot; 问题,使代码更简洁、易维护。本文通过电子商务网站登录状态管理案例,详细介绍了 Context API 的使用方法,包括创建、提供及消费 Context,以及处理多个 Context 的场景,适合各水平开发者学习与应用,提高开发效率和代码质量。
37 0
|
3月前
|
前端开发 JavaScript 中间件
【前端状态管理之道】React Context与Redux大对决:从原理到实践全面解析状态管理框架的选择与比较,帮你找到最适合的解决方案!
【8月更文挑战第31天】本文通过电子商务网站的具体案例,详细比较了React Context与Redux两种状态管理方案的优缺点。React Context作为轻量级API,适合小规模应用和少量状态共享,实现简单快捷。Redux则适用于大型复杂应用,具备严格的状态管理规则和丰富的社区支持,但配置较为繁琐。文章提供了两种方案的具体实现代码,并从适用场景、维护成本及社区支持三方面进行对比分析,帮助开发者根据项目需求选择最佳方案。
46 0
|
3月前
|
前端开发 API
React 中 Context 的概念
【8月更文挑战第31天】
37 0