学习目标
- 组件如何通讯
- props ★ ★ ★ ★
- 组件通讯方式:父传子、子传父、兄弟传值、 ★ ★ ★ ★ ★
- Context组件通讯
组件通讯
示例图:
在 React 项目里面,整个应用、系统,都是由不同组件组成的。就像搭积木一样,一块一块堆积完成,每个组件都有自己内部的数据,例如:属性、函数方法、等等。组件之间是独立的,组件内部修改方法,属性,相互之间是不受影响的。正因为如此,多个组件组合成一个新功能、页面,避免不了通讯。例如:父组件通知子组件,该做什么任务。子组件完成该项任务,就通知父组件完成任务了。以此类推,兄弟组件也会有相对应的通讯方式。
props
如果学过 Vue 的同学,估计看到 props 属性是不会陌生的,没学过也没关系,简单描述一下,就是用来接受父组件传值的, React 功能也一样,就是组件用来接受外部数据的。
需要注意几点:
- props 是组件用来接收外部组件任意数据,通常是父组件传值。
- props 传进来的数据只读,不可修改,跟 state 数据相反。state 数据可修改,state 属于自身组件内部数据。(举个例子:props 相当于进口半导体芯片,state 自研芯片。进口芯片只能使用,不可修改,基于技术壁垒,改动就用不了。 state 属于自主研发,工程师可以随意改动。)
- props用法:
- 父传递数据:给组件标签添加属性。
- 接收数据:函数组件通过参数props接收数据,类组件通过this.props接收数据
函数组件
// 子组件接收组件
function Hi(props) {
return (
<div>接收数据:{props.name}</div>
)
}
// 父组件传值, (给组件标签添加属性:name,子组件能收到该数据)
<Hi name='程序员米粉'/>
类组件
// 子组件接收组件
class Hi extends Component {
constructor(props) {
// 将props传递给父类构造函数, 否则该构造函数,获取不到props的值
super(props)
}
render() {
return <div>接收数据:{this.props.name}</div>
}
}
// 父组件传值, (给组件标签添加属性:name,子组件能收到该数据)
<Hi name='程序员米粉'/>
Props 验证
Props 验证使用 propTypes, 它可以保证我们组件收到的数据格式都是符合标准的。propTypes 提供了很多验证器来验证数据是否符合格式。
App组件
import PropTypes from 'prop-types' // 记得引入。
class App extends React.Component {
render() {
return <div>
Hello, { this.props.title}
</div>
}
}
// App 组件 props 接收数据的类型
App.propTypes = {
// 约定好接收 title 属性的类型,为 string,否则报错,抛出警告
title: PropTypes.string
}
使用App组件
import App from './App'
const title = '程序员米粉';
// <App title={title}传值,App组件约定接收数据类型 string,传非 string 类型会抛出异常。
ReactDOM.createRoot(document.getElementById('root')).render(<App title={title} />);
特点总结:
- 引入 PropTypes
- MyComponent.propTypes = {} 定义组件接收数据类型。
- 使用 PropTypes 校验规则,例如:PropTypes.string
Props 默认值
场景:分页组件 每页显示条数
作用:给 props 设置默认值,在未传入 props 时生效
function App(props) {
return (
<div>
此处展示props的默认值:{props.pageSize}
</div>
)
}
// 设置默认值
App.defaultProps = {
pageSize: 10
}
// 不传入pageSize属性
<App />
Props 验证约定规则
常见的有:
- 常见类型:array、bool、func、number、object、string
- React元素类型:element
- 必填项:isRequired
- 特定结构的对象:shape({})
- 等等,可以文档查一下
import PropTypes from 'prop-types';
MyComponent.propTypes = {
// 你可以将属性声明为 JS 原生类型,默认情况下
// 这些属性都是可选的。
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,
// 一个 React 元素。
optionalElement: PropTypes.element,
// 一个 React 元素类型(即,MyComponent)。
optionalElementType: PropTypes.elementType,
// 你也可以声明 prop 为类的实例,这里使用
// JS 的 instanceof 操作符。
optionalMessage: PropTypes.instanceOf(Message),
// 可以指定一个对象由特定的类型值组成
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
// An object with warnings on extra properties
optionalObjectWithStrictShape: PropTypes.exact({
name: PropTypes.string,
quantity: PropTypes.number
}),
// 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
// 这个 prop 没有被提供时,会打印警告信息。
requiredFunc: PropTypes.func.isRequired,
// 任意类型的必需数据
requiredAny: PropTypes.any.isRequired,
};
组件3中通讯方式
- 父传子
- 子传父
- 非父子
父传子
父传子在平常开发比较常见,例如父组件从上往下让子组件展示某些数据。子组件如何接收?看下面例子:
父组件
// 定义父组件
class Parent extends Component {
constructor(props) {
// 推荐将props传递给父类构造函数
super(props)
this.state = {
name: 'Hello 我是从父组件传值过来的'
}
}
render() {
return <div>
// 提供 name 属性数据
父传数据: <Child name={this.state.name} />
</div>
}
}
子组件
// 定义子组件
class Child extends Component {
constructor(props) {
// 推荐将props传递给父类构造函数
super(props)
}
render() {
return <div>子组件接收数据:{this.props.name}</div>
}
}
// 最终页面显示: 父传数据:子组件接收数据:Hello 我是从父组件传值过来的
// 与我们预想的一致
从上述例子,总结父传子特点:
1. 父组件需要在标签上提供 state 数据
2. 给子组件标签添加属性,值为 state 中的数据
3. 子组件中通过 props 接收父组件中传递的数据
子传父
我们假设一个场景,子组件按钮被用户点击了,那子组件需要告诉父组件,父组件对应展示内容。例子:
父组件
class Parent extends Component {
constructor(props) {
// 推荐将props传递给父类构造函数
super(props)
this.state = {
childMsg: ''
}
}
getChildMsg = (msg) => {
this.setState({
childMsg: msg
});
console.log(msg) // 获取子组件数据
}
render() {
return <div>
子组件 <Child getMsg={this.getChildMsg} />
<div>获取子组件信息: {this.state.childMsg}</div>
</div>
}
}
// 子组件按钮点击之后,得到数据。
// 最终显示【获取子组件信息: 子组件 数据】
子组件
class Child extends Component {
constructor(props) {
// 推荐将props传递给父类构造函数
super(props)
this.state = {
msg: '子组件 数据'
};
}
handleClick = () => {
// 发送数据给父组件
this.props.getMsg(this.state.msg);
}
render() {
return (
<div>
<button onClick={this.handleClick}>给父组件信息</button>
</div>
)
}
}
上面点击按钮传给父组件数据例子,利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数。
从上述例子,总结一下子传父特点、:
1. 父组件需要在子组件提供属性上写上回调函数,用来接受子组件数据。例子:getMsg
2. 子组件通过 props 调用回调函数。例子: this.props.getMsg(this.state.msg);
3. 将子组件的数据作为参数传递给回调函数。
兄弟组件
什么叫兄弟组件?两个不是父子组件的关系,但是有共同父组件,称之为兄弟组件。需要注意的是兄弟组件,不一定是同一级别的组件。看示例图:
从图片可以看出来,B 与 C 两个兄弟组件,可以同一个级别,也可以不同级别。但是最终大家,都有一个最近的共同父组件 A。就像我们血缘关系,在同一条村,同一个姓氏,最终都有一个最近的共同祖先。
兄弟之间不能直接互相传递数据,但是可以用状态提升方法实现兄弟组件传递数据。那怎么状态提升?就是把兄弟组件把状态共享到最近的共同父组件内,任意一个兄弟组件都可以通过父组件传递的回调函数来修改共享状态,父组件中共享状态的变化也会通过props向下传递给所有兄弟组件,从而完成兄弟组件之间的通信。
那兄弟组件通讯常用方法:
1. context
2. Redux (后面章节讲)
3. MobX 等等
Context
我们来想一下,<App>
父组件是如何传递数据给 <Child>
组件数据的?
假如是按照以前的方法父子组件传值的方法,我们可以通过父组件往下传值 <App>
=> <Node>
=> <SubNode>
=> <Child>
。
举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:
// 顶层父级组件
class App extends Component {
render() {
return <Toolbar theme="dark" />;
}
}
// Toolbar 子组件
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
// ThemedButton 颜色按钮子组件(我们假设这个是孙组件)
class ThemedButton extends Component {
render() {
return <Button theme={this.props.theme} />;
}
}
从代码看到这个例子,传值是这样的,App
=> Toolbar
=> ThemedButton
, 是不是觉得实在太麻烦了?
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但此种用法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
如何使用Context步骤:
1. React.createContext (创建 Context对象)
2. Context.Provider (提供数据)
3. Context.Consumer (消费数据)
- React.createContext
const { Provider, Consumer } = React.createContext(defaultValue)
// 相当于下面
// const MyContext = React.createContext()
// MyContext.Provider
// MyContext.Consumer
通过 React.createContext()
,创建了一个 Context 对象,Context对象 提供了两个组件,一个 Provider
(提供数据)组件,一个 Consumer
消费数据组件。
- Context.Provider (提供数据)
<Provider value={/* 某个值 */}>
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。
- Context.Consumer (消费数据)
<Consumer>
{value => /* 基于 context 值进行渲染*/}
</Consumer>
一个 React 组件可以订阅 context 的变更,此组件可以让你在函数式组件中可以订阅 context。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。
按钮样式主题改用 Context 方法改造一下:
const { Provider, Consumer } = React.createContext('light');
class App extends Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<Provider value="dark">
<Toolbar />
</Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends Component {
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
render() {
return <div>
<Consumer>
{value => <button theme={value}>{value /** 这里得到父级传的值**/ }</button>}
</Consumer >
</div>
}
}
总结:
1. 如果两个组件有共同最近的父组件,可以使用 Context 实现组件通讯
2. Context 对象 提供了两个组件:Provider 和 Consumer
3. Provider 组件:用来提供数据
4. Consumer 组件:用来消费数据
结语
希望看完这篇文章对你有帮助
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注,后续会输出更好的分享。
欢迎关注公众号:【程序员米粉】
公众号分享开发编程、职场晋升、大厂面试经验