React 非父子组件通信

react.jpg

Context 使用

Context 应用场景

非父子组件数据的共享

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

看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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: 'coderlion',
level: 99
}
}

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

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

上面的代码中App组件将nicknamelevel传递给子组件Profile, 之后又经Profile之手将数据传递给其子组件ProfileHeader, 对于Profile本身来说, 这种操作非常冗余

实际上 JSX 仅仅只是React.createElement(component, props, ...children)函数的语法糖, 详情参见官方文档

下面两种写法是等价的

1
2
3
4
5
6
7
8
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />
}

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

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

1
<ProfileHeader {...this.props} />

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

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

React.createContext

1
const MyContext = React.createContext(defaultValue)

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

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

Context.Provider

1
<MyContext.Provider value={/* ... */} />

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

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

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

Class.contextType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染 */}
</MyContext.Consumer>

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

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

Context 使用过程

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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: 'coderlion', level: 99 }}>
<Profile />
</UserContext.Provider>
<h2>其他内容</h2>
</div>
)
}
}

我们会发现, 这个过程中Profile不需要有任何相关数据传递的操作

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

1
2
<Profile />
<UserContext.Provider value={{ nickname: 'coderlion', level: 99 }} />

Profile并没有作为UserContext.Provider的子组件

什么时候使用Context.Consumer呢?

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

函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function ProfileHeader(props) {
return (
<div>
<UserContext.Consumer>
{value => {
return (
<div>
<h2>用户昵称: {value.nickname}</h2>
<h2>用户等级: {value.level}</h2>
</div>
)
}}
</UserContext.Consumer>
</div>
)
}

当组件中需要使用多个 Context

  1. 创建一个新的 Context
1
const ThemeContext = React.createContext({ color: 'black' })
  1. Provider的嵌套
1
2
3
4
5
<UserContext.Provider value={{ nickname: 'coderlion', level: 99 }}>
<ThemeContext.Provider value={{ color: 'red' }}>
<Profile />
</ThemeContext.Provider>
</UserContext.Provider>
  1. 使用Consumer的嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>

更多用法参照官方文档

事件总线

事件总线的概述

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

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

我们可以通过 NPM 或 Yarn 来安装events

1
yarn add events

Events 常用 API

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

使用 Events

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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', 'coderlion', 20)
}
}

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>
)
}
}
-------------本文结束感谢阅读-------------