第四节:组件通信剖析(父传子、子传父、非父子)、组件插槽的用法剖析
一. 父传子
1. 用法
父组件通过 属性=值 的形式来传递给子组件数据;
子组件通过 props 参数获取父组件传递过来的数据, 详见child1函数组件 和 child2 类组件
2. PropTypes实现传递参数验证
(详见官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html)
即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证;从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
(1). 参数类型的限制
A. 引入 import PropTypes from "prop-types";
B. 通过 PropTypes.string.isRequired 等,进行类型限制 详见:Child1组件
如果传递的不符合,会报警告,如:
id prop `age` of type `string` supplied to `Child1`, expected `number`.
(2). 默认赋值
通过 defaultProps 实现,详见 child1组件
父组件代码:
查看代码
class App extends Component {
constructor() {
super();
this.state = {
name: "ypf",
age: 18,
hobby: ["上网", "打球", "打游戏"],
};
}
render() {
const { name, age, hobby } = this.state;
return (
<div>
<h2>我是父组件</h2>
<Child1 name={name} age={age}></Child1>
{/* 使用默认值,不进行显式传值 */}
<Child1></Child1>
<Child2 hobby={hobby}></Child2>
{/* 使用默认值,不进行显式传值 */}
<Child2></Child2>
</div>
);
}
}
子组件-类组件:
查看代码
// 类子组件
import PropTypes from "prop-types";
import React, { Component } from "react";
class Child2 extends Component {
render() {
const { hobby } = this.props;
return (
<div>
<h3>我是函数子组件-child2</h3>
<ul>
{hobby?.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
}
Child2.propTypes = {
hobby: PropTypes.array.isRequired,
};
// 对传递的参数进行默认赋值
Child2.defaultProps = {
hobby: ["test1", "test2"],
};
子组件-函数式组件
查看代码
import PropTypes from "prop-types";
// 函数-子组件
function Child1(props) {
// 获取父组件传递的值
const { name, age } = props;
// 返回值和类组件中的render是一致的
return (
<div>
<h3>我是函数子组件-child1</h3>
<h4>name:{name}</h4>
<h4>age:{age}</h4>
</div>
);
}
// 对父组件传递过来的参数进行验证
Child1.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
};
// 对传递的参数进行默认赋值
Child1.defaultProps = {
name: "二胖",
age: 33,
};
二. 子传父
1. 用法
(在vue中是通过自定义事件来完成的)
在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可。
父组件:
<AddCounter addClick={count => this.changeCounter(count)}></AddCounter>
子组件:
console.log(this.props.addClick); //是一个函数 count => this.changeCounter(count)
this.props.addClick(count);// 调用父组件的addClick属性,而该属性对应的是一个回调函数
2. 案例实操
(1). 需求
将计数器案例进行拆解;将按钮封装到子组件中:AddCounter和 SubCounter;当子组件发生点击事件,将内容传递到父组件中,修改counter的值
(2). 剖析
父组件通过自定义属性 addClick 绑定一个回调函数,在这个函数中修改counter的值
子组件通过 this.props.addClick 拿到这个属性,这个属性就是要个回调函数,调用它向它传值即可
这样就实现了子组件 传值给 父组件
父组件代码
查看代码
class App extends Component {
constructor() {
super();
this.state = { counter: 100 };
}
changeCounter(count) {
this.setState({ counter: this.state.counter + count });
}
render() {
const { counter } = this.state;
return (
<div>
<h4>{counter}</h4>
<AddCounter addClick={count => this.changeCounter(count)}></AddCounter>
<SubCounter subClick={count => this.changeCounter(count)}></SubCounter>
</div>
);
}
}
子组件代码-AddCounter
查看代码
class AddCounter extends Component {
addCount(count) {
console.log(this.props.addClick); //是一个函数 count => this.changeCounter(count)
// 调用父组件的addClick属性,而该属性对应的是一个回调函数
this.props.addClick(count);
}
render() {
return (
<div>
<button onClick={() => this.addCount(1)}>+1</button>
<button onClick={() => this.addCount(10)}>+10</button>
<button onClick={() => this.addCount(100)}>+100</button>
</div>
);
}
}
子组件代码-SubCounter
查看代码
class SubCounter extends Component {
subCount(count) {
console.log(this.props.subClick); //this.props.subClick属性是一个回调函数, count => this.changeCounter(count)
this.props.subClick(count);
}
render() {
return (
<div>
<button onClick={() => this.subCount(-1)}>-1</button>
<button onClick={() => this.subCount(-10)}>-10</button>
<button onClick={() => this.subCount(-50)}>-50</button>
</div>
);
}
}
三. 非父子--context
1. 说明
React提供了一个API:Context;
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据
2. 实操
(1). 有多个组件,最外层组件是App, App中引用Father组件 和 Son3组件, Father组件中引用Son1 和 Son2组件, 需要实现App组件直接向 Son1、Son2组件传值。
(2). 通过 React.createContext 创建要给Context,如:UserContext
(3). 在 App组件中通过 <UserContext.Provider>包裹Father组件 [必须包裹!],并通过value属性传递数据
(4). 如何接收传递过来的值呢?有两种方法:
方案A:(eg:Son1)
a. 设置接收组件的contextType等于上述创建的Context, Son1.contextType = UserContext;
b. 然后直接通过 this.context 获取传递过来的值即可
方案B: (eg:Son2)
通过 <UserContext.Consumer>包裹,直接使用value获取值即可
补充:如果不包裹,只能使用默认值,eg:ThemeContext 和 Son3
这里具体的代码不贴出来了,太多了,详见code了
3. 剖析
(1). React.createContext
创建一个需要共享的Context对象:
如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
(2). Context.Provider
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
Provider 接收一个 value 属性,传递给消费组件;
一个 Provider 可以和多个消费组件有对应关系;
多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
(3). Class.contextType
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
这能让你使用 this.context 来消费最近 Context 上的那个值;
你可以在任何生命周期中访问到它,包括 render 函数中;
(4).Context.Consumer
这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
这里需要 函数作为子元素(function as child)这种做法;
这个函数接收当前的 context 值,返回一个 React 节点;
(5).什么时候使用Context.Consumer呢?
A. 当使用value的组件是一个函数式组件时;
B. 当组件中需要使用多个Context时;
四. 非父子--Eventbus
1. 前言
这里可以使用 mitt 或者 coderwhy封装的hy-event-store
hy-event-store参考:https://www.npmjs.com/package/hy-event-store
下面以mitt为例测试,参考之前vue系列:https://www.cnblogs.com/yaopengfei/p/15266097.html
2. react中实操
(1). 有三个组件,分别是 App→Father→Son三个组件,App是最上级组件,引用Father组件,Father中引用Son组件,现在要实现 App组件和Son组件的通信。
(2). mitt安装和方法剖析
【npm install mitt】
emit:发送事件的方法
on:监听事件的方法,如果监听所有,可以使用 *
off:卸载事件的方法
emitter.all.clear(): 表示卸载所有事件
(3). App组件中进行如下操作:
点击按钮发送事件 userInfo1 和 userInfo2
(4). Son组件中进行如下操作
在生命周期函数componentDidMount中监听事件 userinfo1 和 全部 * 事件
在生命周期函数componentWillUnmount中卸载事件,如果卸载事件,需要把上面监听事件中的回调函数单独抽离出来
eventBus代码:
import mitt from "mitt";
const emitter = mitt();
export default emitter;
App组件代码:
查看代码
import React, { Component } from "react";
import Father from "./Father";
import emitter from "./utils/eventBus";
export class App extends Component {
sendMsg1() {
// 发送消息
emitter.emit("userInfo1", { name: "ypf", age: 18 });
}
sendMsg2() {
// 发送消息
emitter.emit("userInfo2", { name: "二胖", age: 32 });
}
render() {
return (
<div>
<h2>我是最外层的根组件</h2>
<p>
{/* 这里sendMsg中不使用this,所以这里就不解决this问题了 */}
<button onClick={this.sendMsg1}>App发送消息1</button>
<button onClick={this.sendMsg2}>App发送消息2</button>
</p>
<Father></Father>
</div>
);
}
// 生命周期:组件从DOM中卸载掉
componentWillUnmount() {
console.log("---------App componentWillUnmount-----------------");
}
}
export default App;
Father组件代码:
查看代码
import React, { Component } from "react";
import Son from "./Son";
export class Father extends Component {
render() {
return (
<div>
<h3>我是Father组件</h3>
<Son></Son>
</div>
);
}
}
export default Father;
Son组件代码:
查看代码
import React, { Component } from "react";
import emitter from "./utils/eventBus";
export class Son extends Component {
render() {
return (
<div>
<h5>我是Son组件</h5>
</div>
);
}
// 抽离回调方法
func1(data) {
console.log("------------Son中监听userInfo1-----------------------");
console.log(data);
}
func2(type, data) {
console.log("------------Son中监听所有事件-----------------------");
console.log(type); //userInfo1 或 userInfo2
console.log(data);
}
//生命周期:组件被渲染到DOM
componentDidMount() {
// 不考虑用完卸载的写法--直接写回调即可
/*
emitter.on("userInfo1", data => {
console.log("------------Son中监听userInfo1-----------------------");
console.log(data);
});
emitter.on("*", (type, data) => {
console.log("------------Son中监听所有事件-----------------------");
console.log(type); //userInfo1 或 userInfo2
console.log(data);
});
*/
// 考虑卸载的写法
emitter.on("userInfo1", this.func1);
emitter.on("*", this.func2);
}
// 生命周期:组件从DOM中卸载掉
componentWillUnmount() {
console.log("---------Son componentWillUnmount-----------------");
// 用完后最好卸载, 如果要卸载的话,就不能直接写回调了,就需要把回调单独封装成一个函数
emitter.off("userInfo1");
// emitter.all.clear(); //清空所有事件
}
}
export default Son;
五. 组件插槽使用
1. 插槽的作用
在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素。
我们应该让使用者可以决定某一块区域到底存放什么内容,在vue中,通过slot实现。
2. 插槽基本使用
在React中,有两种方案可以实现:
方案1: 组件的children子元素
子组件中通过 this.props中的children属性,获取传递过来的元素,分两种情况
(1). 如果传递的是多个元素,那么children是一个数组 (eg: nav-bar-one/index)
(2). 如果传递的是一个元素,那么children就是该元素,不是数组 (eg: nav-bar-one/index2)
父组件核心代码
{/* 1. 使用children实现插槽 */}
<NavBarOne>
<button>按钮1</button>
<h4>我是h4哦</h4>
<i>斜体文本</i>
</NavBarOne>
<NavBarOne2>
<button>按钮1</button>
</NavBarOne2>
NavBarOne代码
export class NavBarOne extends Component {
render() {
const { children } = this.props;
// 特别注意:如果传入的是多个元素,那么children是数组;如果只有一个元素,那么就是children该元素了
console.log(children);
return (
<div className="nav-bar">
<div className="left">{children[0]}</div>
<div className="center">{children[1]}</div>
<div className="right">{children[2]}</div>
</div>
);
}
}
NavBarOne2代码
export class NavBarOne2 extends Component {
render() {
const { children } = this.props;
// 特别注意:如果传入的是多个元素,那么children是数组;
// 如果只有一个元素,那么就是children该元素了
console.log(children);
return (
<div className="nav-bar">
<div className="left">{children}</div>
<div className="center"></div>
<div className="right"></div>
</div>
);
}
}
方案2: props属性传递React元素
父组件中通过自定义属性传递元素,比如 leftSlot={<button>按钮2</button>}
子组件中通过 this.props.leftSlot 获取这个自定义属性,即可以拿到所谓的"插槽内容"
父组件核心代码
const myBtn2 = <button>按钮2</button>;
{/* 2. 通过 自定义属性+props 实现插槽 */}
<NavBarTwo
leftSlot={myBtn2}
centerSlot={<h4>我是h4哦1</h4>}
rightSlot={<i>斜体文本</i>}
></NavBarTwo>
子组件代码
export class NavBarTwo extends Component {
render() {
// 通过props获取自定义属性,从而获取传递过来的插槽内容
const { leftSlot, centerSlot, rightSlot } = this.props;
return (
<div className="nav-bar">
<div className="left">{leftSlot}</div>
<div className="center">{centerSlot}</div>
<div className="right">{rightSlot}</div>
</div>
);
}
}
3. 作用域插槽
(1) 背景
我们希望父组件插槽的位置可以访问子组件中内容。
在vue中,子组件在<slot>中通过自定义属性传值,父组件通过 v-slot="obj", obj就是那些自定义属性的对象。(详见:https://www.cnblogs.com/yaopengfei/p/15338752.html)
(2) react中实现
使用是 组件通信--子传父 的思路,详见案例即可
父组件:
查看代码
import React, { Component } from "react";
import TabControl from "./TabControl";
export class App extends Component {
constructor() {
super();
this.state = {
titles: ["流行", "新款", "精选"],
tabIndex: 0,
};
}
tabClick(tabIndex) {
this.setState({ tabIndex });
}
getTabItem(item) {
if (item === "流行") {
return <span>{item}</span>;
} else if (item === "新款") {
return <button>{item}</button>;
} else {
return <i>{item}</i>;
}
}
render() {
const { titles, tabIndex } = this.state;
return (
<div className="app">
<TabControl
titles={titles}
tabClick={i => this.tabClick(i)}
itemType={item => this.getTabItem(item)}
/>
<h1>{titles[tabIndex]}</h1>
</div>
);
}
}
export default App;
子组件:
查看代码
import React, { Component } from "react";
import "./style.css";
export class TabControl extends Component {
constructor() {
super();
this.state = {
currentIndex: 0,
};
}
itemClick(index) {
// 1.自己保存最新的index
this.setState({ currentIndex: index });
// 2.让父组件执行对应的函数
this.props.tabClick(index);
}
render() {
const { titles, itemType } = this.props;
const { currentIndex } = this.state;
return (
<div className="tab-control">
{titles.map((item, index) => {
return (
<div
className={`item ${index === currentIndex ? "active" : ""}`}
key={item}
onClick={e => this.itemClick(index)}
>
{/* <span className='text'>{item}</span> */}
{itemType(item)}
</div>
);
})}
</div>
);
}
}
export default TabControl;
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。