【React系列】非父子组件通信—Context.Provider共享数据、events库事件总线通信

本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)

一. Context使用

1.1. Context应用场景

非父子组件数据的共享:

  • 在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递。
  • 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
  • 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

我们来看一个例子:

javascript 复制代码
import React, { Component } from 'react';

function ProfileHeader(props) {
  return (
    <div>
      <h2>用户昵称: {props.nickname}</h2>
      <h2>用户等级: {props.level}</h2>
    </div>
  )
}

class Profile extends Component {
  render() {
    return (
      <div>
        <ProfileHeader nickname={this.props.nickname} level={this.props.level} />
        <ul>
          <li>设置1</li>
          <li>设置2</li>
          <li>设置3</li>
          <li>设置4</li>
          <li>设置5</li>
        </ul>
      </div>
    )
  }
}

export default class App extends Component {
  constructor() {
    super();

    this.state = {
      nickname: "coderwhy",
      level: 99
    }
  }

  render() {
    const { nickname, level } = this.state;

    return (
      <div>
        <Profile nickname={nickname} level={level} />
        <h2>其他内容</h2>
      </div>
    )
  }
}

我这边顺便补充一个小的知识点:Spread Attributes

下面两种写法是等价的:

javascript 复制代码
function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

那么我们上面的Profile的传递代码可以修改为如下代码:

javascript 复制代码
<ProfileHeader {...this.props}/>

但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

  • React提供了一个API:Context
  • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
  • Context 设计目的是为了共享那些对于一个组件树而言是"全局"的数据,例如当前认证的用户、主题或首选语言;

1.2. Context相关的API

React.createContext

javascript 复制代码
const MyContext = React.createContext(defaultValue);

创建一个需要共享的Context对象:

  • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
  • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值

Context.Provider

javascript 复制代码
<MyContext.Provider value={/* 某个值 */}>
	...
</MyContext.Provider>

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:

  • Provider 接收一个 value 属性,传递给消费组件;
  • 一个 Provider 可以和多个消费组件有对应关系;
    多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;

Providervalue 值发生变化时,它内部的所有消费组件都会重新渲染;

Class.contextType

javascript 复制代码
class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* 基于 MyContext 组件的值进行渲染 */
  }
}
MyClass.contextType = MyContext;

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:

  • 这能让你使用 this.context 来消费最近 Context 上的那个值;
  • 你可以在任何生命周期中访问到它,包括 render 函数中;

Context.Consumer

javascript 复制代码
<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context

  • 这里需要 函数作为子元素(function as child)这种做法;
  • 这个函数接收当前的 context 值,返回一个 React 节点;

1.3. Context使用过程

我们先按照前面三个步骤来使用一个Context

javascript 复制代码
import React, { Component } from 'react';

const UserContext = React.createContext({ nickname: "默认", level: -1 })

class ProfileHeader extends Component {
  render() {
    return (
      <div>
        <h2>用户昵称: {this.context.nickname}</h2>
        <h2>用户等级: {this.context.level}</h2>
      </div>
    )
  }
}

ProfileHeader.contextType = UserContext;

class Profile extends Component {
  render() {
    return (
      <div>
        <ProfileHeader />
        <ul>
          <li>设置1</li>
          <li>设置2</li>
          <li>设置3</li>
          <li>设置4</li>
          <li>设置5</li>
        </ul>
      </div>
    )
  }
}

export default class App extends Component {
  render() {
    return (
      <div>
        <UserContext.Provider value={{ nickname: "why", level: 99 }}>
          <Profile />
        </UserContext.Provider>
        <h2>其他内容</h2>
      </div>
    )
  }
}
  • 我们就会发现,这个过程中Profile是不需要有任何的数据传递的。

什么时候使用默认值 defaultValue 呢? 如果出现了如下代码:

javascript 复制代码
<Profile />
<UserContext.Provider value={{ nickname: "why", level: 99 }}>
</UserContext.Provider>
  • <Profile />并没有作为 UserContext.Provider 的子组件;

什么时候使用 Context.Consumer 呢?

  1. 当使用value的组件是一个函数式组件时;
  2. 当组件中需要使用多个Context时;

演练一:

javascript 复制代码
function ProfileHeader(props) {
  return (
    <div>
      <UserContext.Consumer>
        {value => {
          return (
            <div>
              <h2>用户昵称: {value.nickname}</h2>
              <h2>用户等级: {value.level}</h2>
            </div>
          )
        }}
      </UserContext.Consumer>
    </div>
  )
}

演练二:当使用value的组件是一个函数式组件时;

  1. 创建一个新的Context
javascript 复制代码
const ThemeContext = React.createContext({ color: "black" });
  1. Provider的嵌套
javascript 复制代码
<UserContext.Provider value={{ nickname: "why", level: 99 }}>
  <ThemeContext.Provider value={{color: "red"}}>
    <Profile />
  </ThemeContext.Provider>
</UserContext.Provider>
  1. 使用Consumer的嵌套
javascript 复制代码
<UserContext.Consumer>
  {value => {
    return (
      <ThemeContext.Consumer>
        {
          theme => (
            <div>
              <h2 style={theme}>用户昵称: {value.nickname}</h2>
              <h2 style={theme}>用户等级: {value.level}</h2>
            </div>
          )
        }
      </ThemeContext.Consumer>
    )
  }}
</UserContext.Consumer>

更多用法可以参考官网:https://zh-hans.reactjs.org/docs/context.html

二. 事件总线

2.1. 事件总线的概述

前面通过Context主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?

  • 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus),来完成操作;
  • 在React中,我们可以依赖一个使用较多的库 events 来完成对应的操作;

我们可以通过npm或者yarn来安装events

javascript 复制代码
yarn add events;

events常用的API:

  • 创建EventEmitter对象:eventBus对象;
  • 发出事件:eventBus.emit("事件名称", 参数列表);
  • 监听事件:eventBus.addListener("事件名称", 监听函数)
  • 移除事件:eventBus.removeListener("事件名称", 监听函数)

2.2. 案例演练

我们通过一个案例来进行演练:

javascript 复制代码
import React, { Component } from 'react';
import { EventEmitter } from "events";

const eventBus = new EventEmitter();

class ProfileHeader extends Component {
  render() {
    return (
      <div>
        <button onClick={e => this.btnClick()}>按钮</button>
      </div>
    )
  }

  btnClick() {
    eventBus.emit("headerClick", "why", 18);
  }
}



class Profile extends Component {
  render() {
    return (
      <div>
        <ProfileHeader />
        <ul>
          <li>设置1</li>
          <li>设置2</li>
          <li>设置3</li>
          <li>设置4</li>
          <li>设置5</li>
        </ul>
      </div>
    )
  }
}

export default class App extends Component {

  componentDidMount() {
    eventBus.addListener("headerClick", this.headerClick)
  }

  headerClick(name, age) {
    console.log(name, age);
  }

  componentWillUnmount() {
    eventBus.removeListener("headerClick", this.headerClick);
  }

  render() {
    return (
      <div>
        <Profile/>
        <h2>其他内容</h2>
      </div>
    )
  }
}
相关推荐
一叶茶1 天前
前端生成docx文档、excel表格、图片、pdf文件
前端·javascript·react
走,板砖去4 天前
大文件传输与断点续传实现(极简Demo: React+Node.js)
node.js·react
SRestia4 天前
Django+React---从0搭建一个听音乐+聊天室的网站
websocket·django·react
远洋录4 天前
前端部署实战:从人工发布到全自动化流程
前端·人工智能·react
Rudon滨海渔村6 天前
React-antd组件库 - 让Menu子菜单项水平排列 - 下拉菜单排序 - 自定义子菜单展示方式
前端·react.js·前端框架·react
远洋录8 天前
前端单元测试实战:从零开始构建可靠的测试体系
前端·人工智能·react
Krorainas8 天前
将PDF流使用 canvas 绘制然后转为图片展示在页面上(二)
前端·javascript·pdf·react
远洋录8 天前
前端性能优化实战:从加载到渲染的全链路提升
前端·人工智能·react
Krorainas10 天前
将PDF流使用 canvas 绘制展示在页面上(一)
前端·javascript·pdf·react
远洋录10 天前
状态管理实战:一次 Redux 到 React Query 的重构之旅
前端·人工智能·react