一、 父子组件间通信
1.1 子组件向父组件进行通信
某些情况,我们也需要子组件向父组件传递消息:
- 在vue中是通过自定义事件来完成的;
- 在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
父组件
import React, { Component } from 'react';
import Cpn from './Cpn'
class App extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return (
<div>
<h3>{ this.state.count }</h3>
{/*
如果需要子组件传递状态给父组件
那么其就需要给子组件传递一个事件的引用,
让子组件的事件可以指向父组件中对应的方法
这样调用子组件对应的事件的callback的时候本质上调用的就是父组件中对应的事件
注意: 这个事件是传递给子组件中交由react自定义事件去执行的,所以需要注意内部的this指向
所以只要是react事件去进行的调用,那么就一定要注意内部this的指向
*/}
<Cpn increment={ () => { this.increment() } } />
</div>
)
}
increment() {
// setState 是一个function
this.setState({
count: this.state.count + 1
})
}
}
export default App
子组件
import React, { Component } from 'react'
class Cpn extends Component {
render() {
return (
<div>
{/*
这里不要去调用函数(也就是不要去加()),
这里需要的是一个引用,指向了父组件中对应的函数
在点击之后本质上回调的是父组件中对应传递过来的方法
*/}
<button onClick={ this.props.increment }>
+1
</button>
</div>
)
}
}
export default Cpn
1.2 父子组件通信综合案例
父组件
import React, { Component } from 'react';
import TabBar from './Cpn'
class App extends Component {
constructor(props) {
super(props)
// 对于后期不会发生修改的数据,不需要挂载到state中
// 直接挂载到组件上即可
this.items = ['流行', '新款', '精选']
// 对于后期需要变化的数据,可以挂载到state中
this.state = {
currentItem: '流行'
}
}
render() {
return (
<div>
{/*
change函数是子组件进行调用的
子组件在调用的时候会传递过来一个index作为形参值
虽然在调用父组件内部的change方法来更新界面
*/}
<TabBar items={ this.items } change={ index => this.change(index) } />
<h3>{ this.state.currentItem }</h3>
</div>
)
}
change(index) {
this.setState({
currentItem: this.items[index]
})
}
}
export default App;
子组件
import React, { Component } from 'react';
// 在子组件中引入样式文件
import './style.css'
class Cpn extends Component {
constructor(props) {
super(props)
this.state = {
currentIndex: 0
}
}
render() {
const {
items
} = this.props
const {
currentIndex
} = this.state
return (
<div className="tab-container">
{
items.map((item, index) => (
<div key={item}
className={'tab-item ' + (index === currentIndex ? 'active' : '') }
onClick={e => this.changeIndex(index) }
>
{/*
1. 如果属性值比较多的时候,其可以采用上述的方法进行换行显示
2, 如果需要动态绑定样式类的时候,需要使用上述的这种字符串拼接的方式
注意多个样式之间使用空格进行划分,所以tab-item后面需要加上空格
3. onClick是react中回调的事件,其只有一个形参,那就是event对象
4. changeIndex事件中的index是循环中的index
*/}
<span> { item } </span>
</div>
))
}
</div>
)
}
changeIndex(index) {
// 修改界面中高亮的元素
this.setState({
currentIndex: index
})
// 调用父组件的方法,修改父组件中的状态
this.props.change(index)
}
}
export default Cpn;
样式文件
.tab-container {
display: flex;
height: 45px;
line-height: 45px;
}
.tab-item {
flex: 1;
text-align: center;
}
.active {
color: red;
}
.tab-item span {
padding: 5px 8px;
}
.active span {
border-bottom: 3px solid red;
}
二、 React模拟插槽
方式1
父组件
import React, { Component } from 'react'
import Cpn from './Cpn'
export default class componentName extends Component {
render() {
return (
<div>
{/*
主要把需要展示的内容作为子组件的子组件去进行传递就可以了
*/}
<Cpn>
<span>Left</span>
<span>Center</span>
<span>Right</span>
</Cpn>
</div>
)
}
}
子组件
import React, { Component } from 'react'
import './style.css'
export default class componentName extends Component {
render() {
return (
<div className="container" >
{/*
此时 Cpn中的所有插槽都作为了props的children给传递过来了
this.props.children 是一个数组,所以我们可以通过索引来获取到每一个插槽值
*/}
<div className="left">
{ this.props.children[0] }
</div>
<div className="center">
{ this.props.children[1] }
</div>
<div className="right">
{ this.props.children[2] }
</div>
</div>
)
}
}
效果图
问题
-
这里获取插槽中的元素的值的时候,使用的是索引的方式,所以对于传入的插槽的顺序是有要求的
如果传入的顺序不对,那么插槽最终展示效果也会出现问题
-
所以 一般使用这种方式去模拟插槽的时候,适合于那些只有一个插槽项的情况,因为其只有一个子组件
方式2
父组件
import React, { Component } from 'react'
import Cpn from './Cpn'
export default class componentName extends Component {
render() {
return (
<div>
{/*
因为JSX最后会被解析为ReactElement对象
所以可以认为JSX就是一个对象类型的数据
所以其可以作为大括号中的值传递给子组件
注意: 这里的插槽中的值可以是类似于<NavBar/>之类的组件,所以可以在这里传递入一个组件(组件最后返回的也是JSX对象)
也可以将这里的JSX对象提取出来形参render函数的变量,随后在Cpn中使用变量即可
*/}
<Cpn
leftSlot={ <span>Left</span> }
centerSlot={ <span>Center</span> }
rightSlot={ <span>Right</span> }
/>
</div>
)
}
}
子组件
import React, { Component } from 'react'
import './style.css'
export default class componentName extends Component {
render() {
// 从props中取出对应的属性值(这里就是插槽值)
let {
leftSlot,
centerSlot,
rightSlot,
} = this.props
// 可以对插槽值进行校验和设置默认值
leftSlot = leftSlot || <span>Slot不存在</span>
centerSlot = centerSlot || <span>Slot不存在</span>
rightSlot = rightSlot || <span>Slot不存在</span>
return (
<div className="container">
<div className="left">
{ leftSlot }
</div>
<div className="center">
{ centerSlot }
</div>
<div className="right">
{ rightSlot }
</div>
</div>
)
}
}
三、跨组件通信
非父子组件数据的共享:
- 在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递。
- 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
- 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的 操作。
但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
- React提供了一个API:Context;
- Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
- Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;
3.1 Context相关API
React.createContext
创建一个需要共享的Context对象:
- 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
- defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
Context.Provider
- 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
- Provider 接收一个 value 属性,传递给消费组件;
- 一个 Provider 可以和多个消费组件有对应关系;
- 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
- 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
Class.contextType
- 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
- 这能让你使用 this.context 来消费最近 Context 上的那个值;
- 你可以在任何生命周期中访问到它,包括 render 函数中;
Context.Consumer
- 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
- 这里需要 函数作为子元素(function as child)这种做法;
- 这个函数接收当前的 context 值,返回一个 React 节点;
3.2 示例代码
1. 使用props一层层嵌套
import React, { Component } from 'react';
// 子组件 === 消费组件
class ProfileHeader extends Component {
render() {
return (
<div>
<h3>UserName: { this.props.name }</h3>
<h3>Age: { this.props.age }</h3>
</div>
)
}
}
// 中间组件
class Profile extends Component {
render() {
return (
<div>
<ProfileHeader {...this.props} />
<div>profile-item1</div>
<div>profile-item2</div>
<div>profile-item3</div>
<div>profile-item4</div>
<div>profile-item5</div>
</div>
)
}
}
// 父组件 === 生产组件
class App extends Component {
constructor(props) {
super(props)
this.state = {
name: 'Klaus',
age: 23
}
}
render() {
return (
<div>
{/*
Spred Atrtribute 对象展开语法
具体文档可以参考: https://zh-hans.reactjs.org/docs/jsx-in-depth.html
{... this.state} === name='Klaus' age=23
*/}
<Profile {...this.state} />
</div>
);
}
}
export default App;
2. 使用上下文(context)进行传递
类组件
import React, { Component } from 'react';
// 1. 创建执行上下问(命名时推荐首字母大写)
const UserContext = React.createContext({
// 这里书写默认值
name: 'Steven',
age: 18
})
class ProfileHeader extends Component {
// 每一个类组件内部都会有一个叫做context变量, 默认值是{}
// 需要将其和对应的ctx进行关联,以便于对context进行赋值操作
static contextType = UserContext
render() {
return (
<div>
{/* 关联完毕以后就可以使用this.context来进行使用 */}
<h3>UserName: { this.context.name }</h3>
<h3>Age: { this.context.age }</h3>
</div>
)
}
}
class Profile extends Component {
render() {
return (
<div>
<ProfileHeader />
<div>profile-item1</div>
<div>profile-item2</div>
<div>profile-item3</div>
<div>profile-item4</div>
<div>profile-item5</div>
</div>
)
}
}
class App extends Component {
constructor(props) {
super(props)
this.state = {
name: 'Klaus',
age: 23
}
}
render() {
return (
<UserContext.Provider value={ this.state }>
{/*
Provider和Consumer是CTX中提供的组件,直接使用即可
value 是你实际需要传递的共享的数据,类型为Object
*/}
{/*
将消费组件所在的组件树(也就是其在父辈组件)
放置到CTX的生成者中
*/}
<Profile />
</UserContext.Provider>
)
}
}
export default App;
render() {
return (
<div>
<UserContext.Provider value={ this.state }>
</UserContext.Provider>
{/* 如果放在生成者内部,那么其值就会自动取value中的值,如果没有给value中的值就会报错 */}
{/* 但是不在生成者内部其就会使用默认值,也就是Steven和18 */}
<Profile />
</div>
)
}
函数组件
import React, { Component } from 'react';
const UserContext = React.createContext({
name: 'Steven',
age: 18
})
function ProfileHeader() {
return (
<UserContext.Consumer>
{/*
在这里使用消费者组件中传递一个callback
在这个callback函数中返回一个JSX对象
*/}
{
value => {
return (
<div>
{/* 这里的value值就是存储了数据的context */}
<h3>name: {value.name}</h3>
<h3>age: {value.age}</h3>
</div>
)
}
}
</UserContext.Consumer>
)
}
class Profile extends Component {
render() {
return (
<div>
<ProfileHeader />
<div>profile-item1</div>
<div>profile-item2</div>
<div>profile-item3</div>
<div>profile-item4</div>
<div>profile-item5</div>
</div>
)
}
}
class App extends Component {
constructor(props) {
super(props)
this.state = {
name: 'Klaus',
age: 23
}
}
render() {
return (
<div>
<UserContext.Provider value={ this.state }>
<Profile />
</UserContext.Provider>
</div>
)
}
}
export default App;
3.多层Context进行消费
子组件消费多个
context只能使用函数组件是不可以使用类组件的
import React, { Component } from 'react';
const UserContext = React.createContext({
name: 'Steven',
age: 18
})
// 这是另外一个需要共享的数据
const ThemeContext = React.createContext({
color: 'black'
})
function ProfileHeader() {
return (
<UserContext.Consumer>
{/* 这里是第一个消费者 */}
{
value => {
return (
<ThemeContext.Consumer>
{/* 这里是第二个消费者 */}
{
theme => {
return (
<div>
<h3>name: {value.name}</h3>
<h3>age: {value.age}</h3>
<h3>color: { theme.color }</h3>
</div>
)
}
}
</ThemeContext.Consumer>
)
}
}
</UserContext.Consumer>
)
}
class Profile extends Component {
render() {
return (
<div>
<ProfileHeader />
<div>profile-item1</div>
<div>profile-item2</div>
<div>profile-item3</div>
<div>profile-item4</div>
<div>profile-item5</div>
</div>
)
}
}
class App extends Component {
constructor(props) {
super(props)
this.state = {
name: 'Klaus',
age: 23
}
}
render() {
return (
<div>
<UserContext.Provider value={ this.state }>
<ThemeContext.Provider value={ {color: 'red'} }>
<Profile />
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
}
export default App;
备注:
- 多数情况下,会把定义的
context的代码抽取到一个独立的文件中,例如context.js- 如果需要传递的数据比较多的情况下推荐使用
redux,以免出现context多层嵌套的问题- 什么时候使用Context.Consumer呢?
- 当使用value的组件是一个函数式组件时;
- 当组件中需要使用多个Context时;
上一篇 React组件化(上) 下一篇 setState的简单使用