0%

React-组件高级

组件通讯介绍

组件是独立封闭的单元,默认情况下,只能使用组件自己的数据,在组件化过程中,我们将一个完整的功能拆分成多个组件,以及更好的完成整个应用的功能。而在这个过程中,多个组件之间不可避免的要共享某些数据。为了实现这些功能,就需要打破组件独立封闭性,让其与外界沟通,这个过程就是组件通讯

组件通讯介绍

组件的 props

  • 组件是封闭的,要接收外部数据应该通过 props 来实现
  • props的作用:接收传递给组件的数据
  • 传递数据:给组件标签添加属性
  • 接收数据:函数组件通过参数props接收数据。类组件通过this.props接收数据
1
<Hello name="jack" age={19}/>
1
2
3
4
5
6
7
8
9
10
// 在函数组件中接收与传递数据
const Hello = (props) => {
// props 是一个对象
console.log(props);
return (
<div>
<h1>props:{props.name}</h1>
</div>
)
}
1
2
3
4
5
6
7
8
9
10
11
class Hello extends React.Component {
// 在类组件中通过 this.props获取外部传递给组件的数据
render() {
return (
<div>
<h1>props:{this.props.name}</h1>
</div>
)
}
}
ReactDOM.render(<Hello name="jack" />, document.getElementById('root'));

特点

1.可以传递任意类型的数据(函数、DOM、数组等…)

2.props只读的对象,只能读取属性的值,无法修改对象的值

3.注意:使用过组件类时,如果写了构造函数,应该将 props 传递给super(),否则,无法在构造函数中获取props!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Hello extends React.Component {
// 在类组件中通过 this.props获取外部传递给组件的数据
constructor(props) {
// 推荐将props传递给父类构造器
super(props);
}
render() {
return (
<div>
<h1>props:{this.props.name}</h1>
</div>
)
}
}

组件通讯的三种方式

组件之间的通讯分为3种

1.父组件—>子组件

2.子组件—->父组件

3.兄弟组件

父传子

1.父组件提供要传递的state数据

2.给子组件添加属性,值为state中的数据

3.子组件中通过props接收父组件中传递的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 父组件
class Father extends React.Component {

state = {
mName: '传递给子组件的数据'
}
render() {
let { mName } = this.state;
return (
<div className='parent'>
父组件:
<Child name={mName} />
</div>
)

}
}

const Child = (props) => {
// props 是一个对象
// 且是一个只读属性不能去修改他的值
console.log(props);
return (
<div className="child">
<h1>接收到父组件传递的数据:{props.name}</h1>
</div>
)
}

子传父

思路:利用回调函数,父组件提供回调,子组件调用,将要传递数据作为回调函数的参数。

1.父组件提供一个回调函数(用于接收数据)

2.构造函数作为属性的值,传递给子组件

3.子组件通过 props 调用回调函数

4.将子组件的数据作为参数传递给回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

// 父组件
class Father extends React.Component {

state = {
mName: '传递给子组件的数据'
}
// 接收子组件的数据
getChildMsg = (msg) => {
console.log(msg);
}
render() {
let { mName } = this.state;
return (
<div className='parent'>
父组件:
<Child getMsg={this.getChildMsg} />
</div>
)

}
}

class Child extends React.Component {

state = {
childMsg: 'childMsg'
}
toFather = () => {
let { childMsg } = this.state;
this.props.getMsg(childMsg);

}

render() {
return (
<div className='child'>
子组件: <button onClick={this.toFather}>点击,传递数据给子组件</button>
</div>
)
}
}
ReactDOM.render(<Father />, document.getElementById('root'));

注意:回调函数中this指向问题!

兄弟组件

  • 共享状态提升到最近的公共父组件中,有公共父组件管理这个状态
  • 思想:状态提示

兄弟组件通讯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';


// 父组件
class Father extends React.Component {

state = {
msg: ''
}
// 接收子组件的数据
getChildMsg = (msg) => {
console.log('接收到子组件传递的数据' + msg);
this.setState({
msg
})
}
render() {
let { msg } = this.state;
return (
<div className='parent'>
父组件:
<Child1 getMsg={this.getChildMsg} />
<Child2 msg={msg} />
</div>
)

}
}

class Child1 extends React.Component {

state = {
msg: '给child2的数据'
}
toFather = () => {
let { msg } = this.state;
this.props.getMsg(msg);

}
render() {
return (
<div className='child'>
子组件1:{this.props.msg}
<button onClick={this.toFather}>传递信息</button>
</div>
)
}


}
class Child2 extends React.Component {

render() {
return (
<div className='child'>
子组件2接收子组件1的数据:{this.props.msg}
</div>
)
}
}
ReactDOM.render(<Father />, document.getElementById('root'));

Context

思考:App组件要传递数据给 Child 组件,该如何处理?

  • 处理方式:使用 props 一层层组件往下传递(繁琐)
  • 更好的的方式:使用Context
  • 作用:跨组件传递数据(比如:主题、语言等)

Context

使用步骤:

1.调用React.createContext() 创建 Provider(提供数据)和Consumer(消费数据)两个组件

1
const {Provider,Consumer} = React.createContext()

2.使用 Provider组件作为父节点。

1
2
3
4
<Provide>
<div className="App">
<Child1/>
</Provide>

3.设置value属性,表示要传递的数据

1
<Provider value="pink">

4.调用Consumer 组件接收数据

1
2
3
<Consumer>
{data=><span>data参数表示接收的数据--{data}</sapn>}
</Consumer>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

const { Provider, Consumer } = React.createContext();
class App extends React.Component {

render() {
return (
<Provider value="pink">
<div className="App">
<Node />
</div>
</Provider>
)
}
}
const Node = props => {
return (
<div className="node">
<SubNode />
</div >
)
}
const SubNode = props => {
return (
<div className="SubNode">
<Child />
</div >
)
}

const Child = props => {
return (
<div className="child">
<Consumer>
{data => <span>data参数表示接收的数据--{data}</span>}
</Consumer>
</div >
)
}

ReactDOM.render(< App />, document.getElementById('root'));

总结:

1.如果两个组件是远方亲戚(比如,嵌套多层)可以使用Context实现组件通讯

2.Context提供了两个组件:和

3.Provider组件:用来提供数据

4.Consumer组件:用来消费数据

props 深入

children 属性

  • children属性:表示组件标签的子节点。当组件标签有子节点时,props就会有该属性
  • children属性:属性与普通的props一样,值可以是任意值(文本、React元素、组件、甚至函数)
1
2
3
4
5
6
7
8
function Hello(props){
return(
<div>
组件子节点:{props.children}
</div>
)
}
<Hello>我时子节点</Hello>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

/**
* children 属性
* 只有当组件标签中存在内容时,才会产生
*/
// const App = props => {
// console.log(props);
// return (
// <div>
// <h1>组件标签的子节点:{props.children}</h1>
// </div>
// )
// }

// ReactDOM.render(<App>我是字节点</App>, document.getElementById('root'));

// 当内容是标签时
// const App = props => {
// console.log(props);
// return (
// <div>
// <h1>组件标签的子节点:{props.children}</h1>
// </div>
// )
// }

// ReactDOM.render(<App><p>我是p标签</p><button>按钮</button></App>, document.getElementById('root'));

// const App = props => {
// console.log(props);
// return (
// <div>
// <h1>组件标签的子节点:{props.children()}</h1>
// </div>
// )
// }

// ReactDOM.render(<App>
// {
// () => console.log("这是一个函数")
// }
// </App>, document.getElementById('root'));

props 校验

  • 对于组件来说,props是外来的,无法保证使用者传入什么格式的数据
  • 如果传入的数据格式不对,可能会导致组件内部报错
  • 关键问题:组件的使用者不知道明确的错误原因
1
2
3
4
5
6
7
8
9
10
 // 创建的组件App
function App(props){
const arr = props.colors;
const lis = arr.map((item,index)=><li key={index}>{item.name}</li>)
return(
<ul>{lis}</ul>
)
}
// 错误的传递数据
<App colors={19}/>
  • props 校验:允许在创建组件的时候,就指定props的类型、格式等
  • 作用:捕获使用组件时因为props导致的的错误,给出明确的错误提示,增加组件的健壮性
1
2
3
App.propTypes = {
colors:PropTypes.array
}

props 校验

使用步骤:

  1. 安装prop-types (yarm add prop-types/ npm i prop-types
  2. 导入 prop-types 包
  3. 使用组件名.propTypes = {}给组件的props添加校验规则
  4. 校验规则通过 PropTypes对象来指定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import PropTypes from 'prop-types';



const App = props => {

console.log(props);
return (
<div>
<h1>组件标签的子节点:{props.colors}</h1>
</div>
)
}
App.propTypes = {
colors: PropTypes.array,
}

约束规则

1.常见类型:array、bool、func、number、object、string

2.React元素类型:element

3.必填项:isReauired

4.特定结构的对象:shape({})

1
2
3
4
5
6
7
8
// 常见类型
optionalFunc: PropTypes.func
// 必须
requiredFunc:PropTypes.func.isReuired,
optionalObjectWithShap:PropTypes.shape({
color:PropTypes.string,
fontSize:PropTypes.number
})

组件的生命周期

概述

  • 意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等
  • 组件的生命周期: 组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
  • 生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数
  • 钩子函数的作用:为开发人员在不同阶段操作的组件提供了时机
  • 只有 类组件 才有生命周期

生命周期的三个阶段

1.每个阶段的执行时机

2.每个阶段钩子函数的执行顺序

3.每个阶段子函数的作用

生命周期

创建时(挂载阶段)

  • 执行时机:组件创建时(页面加载时)

  • 执行顺序

    执行顺序

创建时钩子函数

更新时(更新阶段)

  • 执行时机:setState() forceUpdate() 组件接收到新的props

  • 说明:以上三者任意一种变化,组件就会重新渲染

  • 执行顺序:

    更新时

更新时生命周期

卸载时(卸载阶段)

  • 执行时机:组件从页面中消失

卸载时

不常用钩子函数

旧版本生命周期:

旧版本生命周期

新版本生命周期:

新版本生命周期

render-props

概述

  • 提供组件复用性
  • 通过props.children属性来灵活控制子组件要渲染的内容

通过实现一个切换组件我们来深化一下对 React 理解,我们的研究是从一个示例开始,创建一个切换的按钮来切换部分内容显示和隐藏。
创建一个 button 作为操作切换状态的触发器。

1
<button>显示/隐藏</button>

定义状态 state,页面有两个状态分别是

  • on:true 显示内容
  • on:false 隐藏内容
    通过更新状态 state 来更新界面,状态更新了会更新 dom 。
1
2
3
state = {
on:false,
}

定义 toggle 方法主要用于负责更新 state 中 on 的值来控制页面状态,隐藏/显示界面

1
2
3
4
5
toggle = () => {
this.setState({
on:!this.state.on
})
}

简单实现

1
2
3
4
5
{
this.state.on && (
<h1>Zidea</h1>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from 'react'

export default class Toggle extends Component {
state = {
on:false,
}

toggle = () => {
this.setState({
on:!this.state.on
})
}
render() {
return (
<div className={toggleStyle}>
{
this.state.on && (
<h1>Zidea</h1>
)
}
<button onClick={this.toggle}>显示/隐藏</button>
</div>
)
}
}

const toggleStyle = {
background:"blue"
}

我们的控制要显示或隐藏内容是不是一层不变的,为更灵活的考虑,可能控制内容根据不同情况(场景)会有所不同,我们将这部分内容作为 children 参数传入来提供组件复用性。

1
2
3
4
5
6
7
8
9
10
11
12
render() {
return (
<div className={toggleStyle}>
{
this.state.on && (
this.props.children
)
}
<button onClick={this.toggle}>显示/隐藏</button>
</div>
)
}
1
2
3
4
5
<div>
<Toggle>
<h1>angular basic tut</h1>
</Toggle>
</div>

Render props

是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
render() {
return (
<div>
<ToggleRenderProps render={()=>(
<div>
<h1>tut list</h1>
<button>隐藏/显示</button>
</div>
)}/>
</div>
)
}

然后我们通过 props 方法到作为 props 传入的参数来使用,可以调用 render 函数将界面渲染出来。

1
2
3
4
5
6
7
8
render() {
const { render} = this.props;
return (
<div>
{render()}
</div>
)
}

既然 render 函数,是不是可以将参数参入函数来使用呢?当然没有问题,我们可以尝试在组件内部来控制组件显示。

1
2
3
4
5
6
<ToggleRenderProps render={(str)=>(
<div>
<h1>{str}</h1>
<button>隐藏/显示</button>
</div>
)}/>

这样就顺利将 hello zidea 显示到我们的界面上了。

1
2
3
4
5
6
7
8
render() {
const { render} = this.props;
return (
<div>
{render('hello zidea')}
</div>
)
}

既然可以可以传参数,那么也可以传入一个对象进入界面。

1
2
3
4
5
6
<div>
{render({
on:this.state.on,
toggle:this.toggle
})}
</div>

这样我们 render 方法就可以接收到组件内部 state 和 toggle 方法来控制行为,这样就将功能和界面成功进行分离来实现更灵活的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
render() {
return (
<div>
<ToggleRenderProps render={({on, toggle})=>(
<div>
{
on && <h1>Hey zidea</h1>
}
<button onClick={toggle}>隐藏/显示</button>
</div>
)}/>

</div>
)
}

其实这里 render 我们给其任意名称,render 只是一个名称而已,既然这样我们可以可以用 children ,children 使我们的固有函数

1
2
3
4
5
6
7
render() {
const { children } = this.props;
return children({
on:this.state.on,
toggle:this.toggle
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
render() {
return (
<div>
<ToggleRenderRPC>
{ ({on,toggle}) => (
<div>
{on && <h1>Zidea React</h1>}
<button onClick={toggle}>显示/隐藏</button>
</div>
)}
</ToggleRenderRPC>
</div>
)
}
1
import React, { Component,Fragment } from 'react'

在 react 新特性中提供 Fragment 。(类似Vue 中的插槽 solt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
render() {
return (
<div>
<ToggleRenderRPC>
{ ({on,toggle}) => (
<Fragment>
{on && <h1>Zidea React</h1>}
<button onClick={toggle}>显示/隐藏</button>
</Fragment>
)}
</ToggleRenderRPC>
</div>
)
}

高阶组件

概述

  • 目的:实现状态逻辑复用
  • 采用 包装(装饰)模式,比如说:手机壳
  • 手机:获取保护功能
  • 手机壳:提供保护功能
包装模式

思路分析

  • 高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件
  • 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过props将复用的状态传递给被包装的组件WrappedComponet
1
const EnhancedComponet = withHOC(WrappedComponet)
1
2
3
4
5
6
7
// 高阶组件内部创建的类组件
class Mouse extends React.Component{
render(){
return <WrappedComponent {...this.state}/>
}

}

使用步骤

1.创建一个函数,名称约定以with开头

2.指定函数的参数,参数应该以大写字母开头(作为要渲染的组件)

3.在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回

4.在该组件中,渲染参数组件,同时状态通过props传递给参数组件

5.调用该高阶组件,传入增强的组件,通过返回值拿到增强后的组件,并渲染到页面中

1
2
3
4
function withMouse(WrappedComponent){
class Mouse extends React.Component{}
return Mouse
}
1
2
// Mouse 组件的render方法
return <WrappedComponent {...this.state}/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import PropTypes from 'prop-types';



/**
* 高阶组件
*/
// 创建高阶组件
function withMouse(WrappendComponent) {
// 该组件提供复用的状态逻辑
class Mosue extends React.Component {
// 鼠标位置
state = {
x: 0,
y: 0
}
handleMouserMove = e => {
// console.log(e);
this.setState({
x: e.clientX,
y: e.clientY
})
}

// 控制鼠标状态的逻辑
componentDidMount() {
window.addEventListener('mousemove', this.handleMouserMove)
}
// 解除事件
componentWillUnmount() {
window.removeEventListenr('mousemove', this.handleMouserMove)
}
render() {
return <WrappendComponent {...this.state}></WrappendComponent>
}
}
return Mosue;

}

// 用来测试高阶组件
const Position = props => (
<p>
鼠标当前的位置:x:{props.x},y:{props.y}
</p>
)
// 调用高阶组件
const MousePosition = withMouse(Position);
class App extends React.Component {

constructor(props) {
super(props)
this.state = {
count: 0
}
}


render() {
let { count } = this.state;
return (
<div>
<h1>高阶组件</h1>
<MousePosition />
</div>
)
}

}

ReactDOM.render(<App />, document.getElementById('root'));

设置 displayName

  • 使用高阶组件存在的问题:得到的两个组件名称相同
  • 原因:默认情况下,React使用组件名称作为 displayName
  • 解决方式:为高阶罪案,设置 displayName便于调试时区分不同的组件
  • dispalyName的作用:用于设置调试信息(React Developer Tools信息)
  • 设置方式:
1
2
3
4
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
function getDisplayName(WrappedComponent){
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

传递 props

  • 问题:props丢失
  • 原因:高阶组件没有往下传递 props
  • 解决方式:渲染 WrappedComponent时,将statethis.props一起传递给组件
  • 传递方式:
1
<WrappedComponent {...this.state} {...this.props} />

总结

1.组件通讯是构建 React 应用必不可少的一环

2.props的灵活性让组件更强大

3.状态提升是React组件的常用模式

4.组件生命周期有助于了解组件的运行过程

5.钩子函数让开发着可以在特定的时机执行某些功能

6.render props模式和高阶组件都可以实现组件状态逻辑的复用

7.组件极简模型:(state,props)=>UI

谢谢老板