阅读 217

React组件详解(组件生命周期,组件间各种通信)等

1. react组件化

​ 目前的前端三大框架的核心都是采用组件化的思想。

  • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
  • 任何的应用都可以被抽象为一个组件树

组件化思想的应用:

  • 尽可能的将页面拆分为一个个小的,可复用的组件
  • 是我们的代码更加易于管理,并且扩展性也增强

React的组件相对于Vue的组件更加灵活与多样,按照不同的方式可以分为多类组:

  1. 根据组件的定义方式可以大致分为:函数时组件与类-组件
  2. 根据是否有状态又可以分为无状态组件与有状态组件
  3. 根据不同职责又分为展示性组件和容器型组件

2.类组件与函数组件的创建

类组件与函数组件的主要区别是类组件是有内部状态的而函数组件时没有内部状态的。

2.1 类组件

首先类组件的必须满足两个要求:

  1. 必须继承自React.component
  2. 必须实现render函数

如以下代码所示:

import React, { Component } from 'react'

export default class App2 extends Component {
  //constructor是可选的,我们通常在constructor中初始化一些数据;
  constructor() {
    super()
    //this.state中维护的就是我们组件内部的数据;
    this.state={
    }
  }
  render() {
    return (
      <div>
        
      </div>
    )
  }
}

复制代码

其次我们应当注意的是Render函数的返回值类型:

  1. React元素(ReactElement元素)
  2. 数组或是fragment(使得render方法可以返回多个元素)
  3. portals:可以渲染子节点到不同的DOM子树中
  4. 字符串或者数值类型:会被直接渲染为文本结点
  5. 布尔类型或者null:不显示

2.2 函数组件

​ 函数组件一般时直接使用function来进行定义的,它的返回结果和类组件的返回结果是一样的。

​ 函数组件有以下需要注意的点:

  1. 没有生命周期,虽然会被更新和挂载
  2. 没有this(因此不需要考虑this指向问题)
  3. 没有内部状态(state)

这是一个函数组件的例子:

import React from 'react'
export default function App2() {
  return (
    <div>
    </div>
  )
}

复制代码

3.组件的生命周期

​ react组件的生命周期主要分为三个阶段,大致为

  1. 挂载阶段(Mount)
  2. 更新阶段(update)
  3. 卸载阶段(unmount)

React内部为了告诉我们组件当前的状态处于哪个阶段,会在组件的内部实现某些函数进行回调,也就是我们常说的生命周期函数。我们可以在各个阶段对生命周期函数进行回调并在里面做一些我们的业务操作。

​ 我们可以结合官方给我们的生命周期图谱进行这些生命周期函数的学习。 在这里插入图片描述

我们可以根据上图分析以下组件的整个生命周期的执行流程:

​ 首先在Mounting阶段在挂载阶段会先执行我们的constructor函数来进行一些数据状态的初始化操作,接着执行render函数获取ReactElement元素并且进行DOM的渲染。当组件挂载成功以后会回调componentDidMount函数

​ 接着在Updating阶段,当我们修改了props对象,或者进行setState操作,或是forceUpdate操作,简而言之就是我们修改了数据后UI发生了变化,就会重新调用render函数对组件的dom重新渲染,进行更新操作,当更新完成以后,会回调componentDidUpdate生命周期函数

​ 最后Unmounting阶段,我们的组件不再使用时,会回调componentWillUnmount函数。

列举几个常见的生命周期函数以及使用场景:

  • constructor函数:对state进行初始话;对事件绑定this

  • componentDidMount函数:依赖于DOM的操作;发送一些网络请求;发布一些订阅

  • componentDidUpdate函数:组件更新后,也可以对DOM进行操作;发送网络请求

  • componentWillUnmount函数:执行必要的清除操作,例如定时器等

验证:

import React, { Component } from 'react'

class App1 extends Component {
  render() {
    return (
      <div>
        我是验证conponentWillUnMount的组件
      </div>
    )
  }
  componentDidMount(){
    console.log('我是第二个组件的Mount函数');
  }
  componentWillUnmount(){
    console.log('我是第二个组件的UnMount函数');
  }
}

export default class App extends Component {
  constructor() {
    super();
    this.state={
      num:1,
      flag:true
    }
    console.log('constructor函数时期')
  }
  render() {
    console.log('render函数时期')
    return (
      <div>
        <h2>生命周期函数演示</h2>
        <button onClick={() => {
          this.setState({num:this.state.num+1})
        }}>+1</button>
        <p>{this.state.num}</p>
        <hr/>
        <button onClick={() => {
          this.setState({flag:!this.state.flag})
        }}>点击隐藏第二个组件</button>
        {this.state.flag&&<App1></App1>}

      </div>
    )
  }
  componentDidMount() {
    console.log('componentDidMount时期')
  }
  componentDidUpdate() {
    console.log('componentDidUpdata时期')
  }
}

复制代码

​ 注意:我们生命周期的操作都是在类组件中进行的,因为函数时组件时没有生命周期函数的(Hooks另说)

4. 组件的嵌套

首先父子组件的概念是怎么来的呢?是因为组件的嵌套,什么是组件的嵌套,就是将我们定义的组件嵌入到另一个组件中。就像我们开发一个商城首页,一般需要header,banner,content以及footer组成。因此我们可以这些东西都抽离成一个组件,在直接在一个父容器的组件中使用。 在这里插入图片描述

如下面代码:

import React, { Component } from 'react'
function Header() {
  return (
    <div>
      <h2>我是Header组件</h2>
    </div>
  )
}
function Main() {
  return (
    <div>
      <h2>我是Main组件</h2>
      <Banner></Banner>
      <ProductList></ProductList>
    </div>
  )
}
function Banner() {
  return (
    <div>
      <h3>我是Main_Banner组件</h3>
    </div>
  )
}
function ProductList() {
  return (
    <div>
      <h3>我是Main_list组件</h3>
    </div>
  )
}

function Footer() {
  return (
    <div>
      <h2>我是Footer组件</h2>
    </div>
  )
}

export default class App
  extends Component {
  render() {
    return (
      <div>
        <Header></Header>
        <Main></Main>
        <Footer></Footer>
      </div>
    )
  }
}
复制代码

上面代码我们的子组件都是使用函数式组件,父容器组件使用的是类组件。

4.父子组件间的通信

​ 既然有了组件及其嵌套组件的概念那么组件之间必然是可以进行通信的,举一个简单的例子,我们有一个父子组件,目前子组件需要父组件的数据,那么我们就需要数据的通信。

4.1 父传子

我们在父组件只需要为子组件指定对应的属性即可。

 render() {
    return (
      <div>
        <Person name='aoao' age={2} sex='男'></Person>
      </div>
    )
  }
复制代码

然后再子组件中分如下情况:

当子组件是类组件时:

我们可以再constructor中初始化prop,我们传过来的数据就存在props对象中。

这里值得一提的是,在constructor构造器中,

  1. 我们可以使用this.props=props初始化props对象,
  2. 也可以使用super再父类初始化prop,可以看一下Component源码,已经为我们指向了props
  3. 甚至我们不用初始化
class Person extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    return (
      <div>
        <h2>{this.props.name+""+this.props.age+""+this.props.sex}</h2>
      </div>
    )
  }
}
复制代码

当子组件是函数组件时:

直接给函数一个props参数就可直接使用使用props对象即可

 function Person(props) {
  return (
    <div>	
      <div>
        <h2>{props.name+""+props.age+""+props.sex}</h2>
      </div>
    </div>
  )
}
复制代码

有时候对于传给子组件的数据我们需要数据格式的验证,这个时候就需要propTypes来进行验证 。

4.2 子传父

​ 在react中子组件像父组件传递数据实际上也是利用props来传递消息,只需要让都组件给子组件传递一个回回调函数,然后再子组件中调用这个函数即可。

  • 我们通常利用上述方法将子组件的事件传递给父组件,然后通过子组件里的内容来去操作父组件的state
  • 也可以通过传递参数的形式实现父子组件间的传值通信。

我们可以下面模拟一个TabControl的案例 :

大概的功能就是内容随着tab的切换而切换,并且我们将tab封装为一个组件(TabControl),App组件为我们的父容器,然后将数据和tab的数据保存在App组件中。这样写的目的是可以实现父子组件双方数据的互相传输。

App.js代码:

import React, { Component } from 'react'
import TabControl from './TabControl'
import './style.css'
export default class App extends Component {
  constructor(props) {
    super(props)
    this.title = ['流行', '新款', '精选']
    this.state = {
      currentIndex:0
    }
  }
  render() {
    return (
      <div>
        <TabControl sendEvent={(index) => {
          this.sendEvent(index)
        }} title={this.title}></TabControl>
        <h2>{this.title[this.state.currentIndex]}</h2>
      </div>
    )
  }
  sendEvent(index){
    // 子传父回来的index
    console.log(index)
    this.setState({
      currentIndex:index
    })
  }
}
复制代码

TabControl代码:

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export default class TabControl extends Component {
  static propTypes = {
    title: PropTypes.array.isRequired
  }
  constructor(props) {
    super(props)
    this.state = {
      currentIndex: 0
    }
  }
  render() {
    let { title } = this.props;
    let { currentIndex } = this.state;
    return (
      <div className='tabcontrol'>
        {
          title.map((item, index) => {
            return <div
              onClick={() => {
                this.setState({
                  currentIndex: index
                })
                this.props.sendEvent(index)
              }}
              className={'tabcontrol_item ' + (currentIndex === index ? 'activity' : '')}
              key={index}>
              {item}
              <span className={currentIndex === index ? 'activitx' : ''}></span></div>
          })
        }
      </div>
    )
  }
}

复制代码

6.跨组件通信

​ 在一般情况下我们遇到非父子组件通信时,会使用props对象进行逐层传递,例如我现在有一个组件之间的关系时A—>B—>C,现在A需要给C传递数据,如果要使用我们之前学习的组件间传值进行操作的话,那么可以将A的值通过props传递给B,再将props通过B传递给C,就可以完成此操作。但是如果我们的中间层不用props对象中数据,那么中间层在这里就充当了一个管道的作用,因此这也就是一种数据冗余的现象。

下面代码模拟了一下上面的情况,父组件为App,中间组件为Profile,目标组件为ProfileList,我们要将App中的state传递给目标对象。下面代码中我们使用了props进行传值。

import React, { Component } from 'react'
function ProfileList(props) {
  console.log(props)
  return (
    <div>
      <p>{props.name}</p>
      <p>{props.address}</p>
    </div>
  )
}
function Profile(props) {
  return (
    <div>
      <ProfileList {...props}
      ></ProfileList>
      <p>这是列表内的内容</p>
    </div>
  )
}
export default class App extends Component {
  constructor(props) {
    super(props)
    this.state={
      name:'张三',
      address:'北京'
    }
  }
  render() {
    return (
      <div>
        <Profile {...this.state}></Profile>
      </div>
    )
  }
}

复制代码

那么我们是否有一种更加简单的方法来实现这个操作呢?

这就引出了Context的概念。

一般情况下使用Context有三大步骤:

  1. 创建 React.createContext对象
  2. 使用Context.Provider组件对我们父容器下组件进行包裹,并设置value
  3. 使用class的contexttype属性为组件挂载Context对象。
  4. 被挂载的组件就可以使用this.context消费该Context的值了

举个例子:

import React, { Component } from 'react'
const MyContext = React.createContext()
class ProfileList extends Component {
  render() {
    console.log(this.context)
    return (
      <div>
        <p>{this.context.name}</p>
        <p>{this.context.address}</p>
      </div>
    )
  }
}
class Profile extends Component{
  render(){
  console.log(this.context)
    return (
      <div>
        <ProfileList
        ></ProfileList>
        <p>这是列表内的内容</p>
      </div>
    )
  }
}
 
export default class 使用props进行跨组件通信 extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '张三',
      address: '北京'
    }
  }
  render() {
    return (
      <div>
        <MyContext.Provider value={this.state}>
          <Profile></Profile>
        </MyContext.Provider>
      </div>
    )
  }
}
ProfileList.contextType=MyContext;
Profile.contextType=MyContext;

复制代码

注意:当我们是一个函数时组件时,或者需要多个Context对象时,就需要使用到Context.Consummer了,使用其进行嵌套传值。

注:Context是用来给非父子组件传递数据使用的,但是当我们传递事件时,就需要使用到时间总线(EventBus)了。

事件总线

前面通过Context主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?

  • 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus),来完成操作;

  • 在React中,我们可以依赖一个使用较多的库 events 来完成对应的操作;

    我们可以通过npm或者yarn来安装events:

yarn add events; 
复制代码

events常用的API:

  • 创建EventEmitter对象:eventBus对象;
  • 发出事件:eventBus.emit("事件名称", 参数列表);
  • 监听事件:eventBus.addListener("事件名称", 监听函数)
  • 移除事件:eventBus.removeListener("事件名称", 监听函数)
import React, { Component } from 'react';
import { EventEmitter } from "events";

const eventBus = new EventEmitter();

class ProfileHeader extends Component {
  render() {
    return (
      <div>
        <button onClick={e => this.btnClick()}>按钮</button>
      </div>
    )
  }

  btnClick() {
    eventBus.emit("headerClick", "why", 18);
  }
}

class Profile extends Component {
  render() {
    return (
      <div>
        <ProfileHeader />
        <ul>
          <li>设置1</li>
          <li>设置2</li>
          <li>设置3</li>
          <li>设置4</li>
          <li>设置5</li>
        </ul>
      </div>
    )
  }
}

export default class App extends Component {

  componentDidMount() {
    eventBus.addListener("headerClick", this.headerClick)
  }
  
  headerClick(name, age) {
    console.log(name, age);
  }
  
  componentWillUnmount() {
    eventBus.removeListener("headerClick", this.headerClick);
  }
  render() {
    return (
      <div>
        <Profile/>
        <h2>其他内容</h2>
      </div>
    )
  }
}
复制代码

5. 插槽实现

在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素。

我们应该让使用者可以决定某一块区域到底存放什么内容。

  • React对于这种需要插槽的情况非常灵活;
  • 有两种方案可以实现:children和props;

两种方式(一种是直接使用props的children,另一种是传一个自定义的标签)

实际上也是使用组件间传值来。

我们可以使用模拟一个Tab Bar的插槽:

import React, { Component } from 'react'
import './style.css'
export default class TabBar extends Component {

  render() {
    console.log(this.props)

    let { LeftSlot, MIddleSlot, RightSlot } = this.props
    return (
      <div className='navBar'>
        {
          LeftSlot
        }
        {
          MIddleSlot
        }
        {
          RightSlot
        } 
        
      </div>
    )
  }
}

复制代码

这是使用的方法:

import React, { Component } from 'react'
import TabBar from './TabBar'
import TabBar2 from './TabBar2'

export default class App extends Component {
  render() {
    return (
      <div>
        <TabBar LeftSlot={<p></p>}
          MIddleSlot={<p></p>}
          RightSlot={<p></p>}></TabBar>
        <TabBar LeftSlot={<p></p>}
          RightSlot={<p></p>}>
            <p>12</p>
            <p>12</p>
            <p>12</p>
          </TabBar>

        <TabBar2>
          <p>2</p>
          <p>3</p>
          <p>4</p>
          <p>4</p>
          <p>4</p>
        </TabBar2>
      </div>
    )
  }
}

复制代码

7.refs

首先我们要搞清楚ref是干什么的?

Refs 是一个 获取 DOM节点或 React元素实例的工具。在 React 中 Refs 提供了一种方式,允许用户访问DOM 节点或者在render方法中创建的React元素。

在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:

使用场景:

  1. 对DOM 元素焦点的控制、内容选择或者媒体播放;
  2. 通过对DOM元素控制,触发动画特效;
  3. 通第三方DOM库的集成。

避免使用 refs 去做任何可以通过声明式实现来完成的事情。例如,避免在Dialog、Loading、Alert等组件内部暴露 open(), show(), hide(),close()等方法,最好通过 isXX属性的方式来控制。

关于refs的使用有三种,一种推荐使用第二种:

  1. String类型的refs(已过时)
  2. 使用CreateRefs()
  3. refs的回调函数

代码演示:

import React, { createRef, PureComponent } from 'react'

export default class refs的使用 extends PureComponent {
  constructor(props) {
    super(props)
    this.refDOM=createRef();
    this.reffun=null;
  }
  render() {
    return (
      <div>
        <h2 ref='dd'>123</h2>
        <h2 ref={this.refDOM}>我是对象类型的ref</h2>
        <h2 ref={(ref) => {
          this.reffun=ref
        }}>回调函数</h2>
        <button onClick={() => {
          this.getDOM()
        }}>拿到DOM</button>
      </div>
    )
  }
  getDOM(){
    //使用字符串 不推荐
    console.log(this.refs.dd)
    // 使用对象,推荐
    console.log(this.refDOM.current)
    // 使用回调函数
    console.log(this.reffun)
  }
 
}
复制代码

8.受控组件与非受控组件

8.1 受控组件

在react中,受控组件与非受控组件都是对于表单而言的。

在 HTML 中,表单元素如 <input><textarea><select>通常维护自己的状态,并根据用户输入进行更新。当用户提交表单时,来自上述元素的值将随表单一起发送。

​ 而 React 的工作方式则不同。包含表单的组件将跟踪其状态中的输入值,并在每次回调函数(例如onChange)触发时重新渲染组件,因为状态被更新。以这种方式由 React 控制其值的输入表单元素称为受控组件

React 受控组件更新 state 的流程: (1)可以通过在初始 state 中设置表单的默认值。 (2)每当表单的值发生变化时,调用 onChange 事件处理器。 (3)事件处理器通过合成事件对象 e 拿到改变后的状态,并更新应用的 state。 (4)setState 触发视图的重新渲染,完成表单组件值的更新。

import React, { PureComponent } from 'react'
export default class App extends PureComponent {
  constructor(props) {
    super(props)
    this.state={
      username:''
    }
  }
  render() {
    return (
      <div>
        <div>
          <label htmlFor="username"></label>
          <input type="text" name="" value={this.state.username}  id="username" onChange={(e)=>{this.changedata(e)}} />
        </div>
        <input type="submit" value="提交" onClick={(e)=>{this.handleclick(e)}} />
      </div>
    )
  }
  handleclick(e){
    e.preventDefault();
  }
  changedata(e){
    this.setState({
      username:e.target.value
    })
  }
}
复制代码

以上例子就是我们实现了一个受控组件的过程,因此我们在react中操作表单时需要使用受控组件的方法

我们这样控制我们的表单就是我们的受控组件。

8.2 非受控组件

React官方时不建议我们使用非受控组件的。如果一个表单组件没有 value props(单选按钮和复选框对应的是 checked prop) 时,就可以称为非受控组件。非受控组件一般是使用后ref来操作我们的原生dom来进行控制表单组件

真实一个栗子:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" defaultValue="Bob" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
复制代码
文章分类
前端
文章标签