第四节:组件通信剖析(父传子、子传父、非父子)、组件插槽的用法剖析

一. 父传子

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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2023-04-26 20:46  Yaopengfei  阅读(139)  评论(1编辑  收藏  举报