本文来自#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
也可以嵌套使用,里层的会覆盖外层的数据;
当 Provider
的 value
值发生变化时,它内部的所有消费组件都会重新渲染;
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
呢?
- 当使用
value
的组件是一个函数式组件时; - 当组件中需要使用多个
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
的组件是一个函数式组件时;
- 创建一个新的
Context
javascript
const ThemeContext = React.createContext({ color: "black" });
Provider
的嵌套
javascript
<UserContext.Provider value={{ nickname: "why", level: 99 }}>
<ThemeContext.Provider value={{color: "red"}}>
<Profile />
</ThemeContext.Provider>
</UserContext.Provider>
- 使用
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>
)
}
}