React学习 --- 组件化开发

468 阅读11分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

任何一个人处理信息的逻辑能力都是有限的,所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。

如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解

而前端目前的模块化和组件化都是基于分而治之的思想

什么是组件化开发

如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂, 而且不利于后续的管理以及扩展

但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部 分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。

组件化应用

  • 我们将一个完整的页面分成很多个组件
  • 每个组件都用于实现页面的一个功能块
  • 每一个组件又可以进行细分
  • 组件本身又可以在多个地方进行复用

组件化是React的核心思想, App本身就是一个组件

组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用

任何的应用都会被抽象成一颗组件树

React组件分类

  • 根据组件的定义方式,可以分为: 函数组件(Functional Component )和类组件(Class Component)

  • 根据组件内部是否有状态需要维护,可以分成: 无状态组件(Stateless Component )和有状态组件(Stateful Component)

  • 据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)

函数组件、无状态组件、展示型组件主要关注UI的展示

类组件、有状态组件、容器型组件主要关注数据逻辑

当然还有很多组件的其他划分方式: 如异步组件和高阶组件等

类组件

  • 组件的名称是大写字符开头(无论类组件还是函数组件)
  • 类组件需要继承自 React.Component 或 React.PureComponent
  • 类组件必须实现render函数,render() 方法是 class 组件中唯一必须实现的方法
  • constructor是可选的,我们通常在constructor中初始化一些数据
  • this.state中维护的就是我们组件内部的数据,且当这些数据改变的时候,UI也需要发生相应的改变
import { Component } from 'react'

export default class App extends Component{
  render() {
    return <h2>最简单的类组件</h2>
  }
}

函数组件

函数组件是使用function来进行定义的函数,只是这个函数的返回值是JSX对象

可以认为函数组件是是类组件中的render函数的一种特殊表现形式

所以函数组件(这里指的是不使用React Hook的函数组件)

  1. 没有生命周期,也会被更新并挂载,但是没有生命周期函数
  2. 没有this(组件实例)
  3. 没有内部状态(state)
export default function App() {
  return (
    <h2>我是最简单的函数组件</h2>
  )
}

render的返回值

当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:

  1. React 元素: 通常通过 JSX 创建
  2. 数组或 fragments: 使得 render 方法可以返回多个元素
  3. Portals: 可以渲染子节点到不同的 DOM 子树中
  4. 字符串或数值类型: 它们在 DOM 中会被渲染为文本节点
  5. 布尔类型或 null: 什么都不渲染

生命周期函数

很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期

React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数

我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能

生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:

  • 装载阶段(Mount),组件第一次在DOM树中被渲染的过程
  • 更新过程(Update),组件状态发生变化,重新更新渲染的过程
  • 卸载过程(Unmount),组件从DOM树中被移除的过程

IpD5RC.png

Constructor

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数

constructor中通常只做两件事情:

  • 给 this.state 赋值对象来初始化内部的state
  • 为事件绑定实例(this), 即修正事件中的this指向

componentDidMount

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用

componentDidMount中通常进行哪里操作呢?

  • 依赖于DOM的操作可以在这里进行
  • 发生网络请求
  • 在此处添加一些订阅(需要在componentWillUnmount取消订阅[事件监听])

componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法

  • 当组件更新后,可以在此处对 DOM 进行操作
  • 比较新旧props的变化,执行对应的操作

componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用

  • 此方法中执行必要的清理操作, 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等

除了上述几个比较常用的生命周期函数外, 还有一些不常用的生命周期函数,具体可以点击这里进行查看

组件间通信

如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护

所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件

再将这些组件组合嵌套在一起,最终形成我们的应用程序

所以组件之间的通信就变得尤为的重要

父传子

类组件

import { Component } from 'react'

class Cpn extends Component {
  // 所有传入的props会存放于当前组件的props对象中
  // 以下是派送类默认的构造函数,所以就算不写构造方法
  // 我们依旧可以使用props对象来获取父类传递给子类的数据
  /* constructor(props) {
    super(props)
  } */

  render() {
    return (<div>
      <h2>{ this.props.name }</h2>
      <h2>{ this.props.age }</h2>
    </div>)
  }
}


export default class App extends Component {
  render() {
    return (
      <div>
        <Cpn name="Klaus" age="23" />
      </div>
    )
  }
}

函数组件

import { Component } from 'react'

// 父组件传入的props会直接作为函数组件的参数被传入
function Cpn(props) {
  return (<div>
    <h2>{ props.name }</h2>
    <h2>{ props.age }</h2>
    <h2>{ props.friends.join(',') }</h2>
  </div>)
}

export default class App extends Component {
  render() {
    return (
      <div>
        <Cpn name="Klaus" age={23} friends={['Alex', 'Jhon']} />
      </div>
    )
  }
}

属性校验

对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说

React对应props的类型校验,使用的是prop-types这个库

prop-types这个库原本是React中的一部分,

但是在React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types

具体可以验证的方式和使用方式,可以点击这里进行查看

类组件

import { Component } from 'react'
// 这是React脚手架下载的时候自带的包,不需要单独安装
// 引入库的时候,对象名的首字母是大写的
import PropTypes from 'prop-types'

class Cpn extends Component {
  // 类组件中类型校验的方式有两种设置方式
  // 方式一: Cpn.propTypes = {} ---> 不推荐
  // 方式二: 静态属性

  // 注意: props类型校验的首字母是小写的
  static propTypes = {
    name: PropTypes.string.isRequired, // name属性的值是字符串类型,且是必传的
    age: PropTypes.number,
    friends: PropTypes.array
  }

  // 设置props的默认值
  static defaultProps = {
    // 如果默认值和isRequired同时存在,那么会先设置默认值,再将对应的值传入子组件
    // 也就是说默认值和isRequired同时存在的时候,isRequired会失去存在的意义
    name: 'Steven',
    age: 23,
    friends: ['Alice']
  }


  render() {
    return (<div>
      <h2>{ this.props.name }</h2>
      <h2>{ this.props.age }</h2>
      <h2>{ this.props.friends.join(',') }</h2>
    </div>)
  }
}


export default class App extends Component {
  render() {
    return (
      <div>
        <Cpn name="Klaus" age={23} friends={['Alex', 'Jhon']} />
        {/* 使用默认值 */}
        <Cpn />
      </div>
    )
  }
}

函数组件

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

function Cpn(props) {
  return (<div>
    <h2>{ props.name }</h2>
    <h2>{ props.age }</h2>
    <h2>{ props.friends.join(',') }</h2>
  </div>)
}

// 函数组件没有对应的静态方法和静态属性
// 所以需要手动向函数组件上挂载对应的校验规则和默认值
Cpn.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  friends: PropTypes.array
}

Cpn.defaultProps = {
  age: 23,
  friends: ['Alice']
}

export default class App extends Component {
  render() {
    return (
      <div>
        <Cpn name="Klaus" age={23} friends={['Alex', 'Jhon']} />
         {/* 使用默认值 */}
         <Cpn />
      </div>
    )
  }
}

子传父

某些情况,我们也需要子组件向父组件传递消息:

  • 在vue中是通过自定义事件来完成的
  • 在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可
import { Component } from 'react'

class Btn extends Component {
  render() {
    return (
     <>
      {/*
        和Vue触发自定义事件不一样的是
        React直接使用引用指向,让子组件直接触发父组件传递过来的事件函数
      */}
       <button onClick={ this.props.increment }>+1</button>
     </>
    )
  }
}

export default class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      counter: 0
    }
  }

  render() {
    return (
      <div>
        <h2>{ this.state.counter }</h2>
        {/*
	  需要在父组件修正this指向
	  因为这个方法虽然是在子组件中被调用
          但是实际的逻辑实在父组件中进行的
          所以函数调用的this必须是状态所在的父组件
        */}
        <Btn increment={ () => this.increment() }/>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

阶段案例

需求: 实现一个简单的Tab组件

Cpn/index.css

.tabs {
  width: 100%;
  display: flex;
  justify-content: space-around;
  align-items: center;
}

.tab.active {
  color: red;
}

.tab span {
  padding: 5px 8px;
}

.tab.active span {
  border-bottom: 3px solid #ff0000;
}

Cpn/index.js

import { Component } from 'react'
import PropTypes from 'prop-types';

import './index.css'

export default class Tabs extends Component {
  static propTypes = {
    tabs: PropTypes.array.isRequired,
    active: PropTypes.string,
    changeActive: PropTypes.func
  }

  static defaultProps = {
    active: 'one'
  }

  render() {
    const { tabs, active, changeActive } = this.props

    return (
      <div className="tabs">
        {
          tabs.map(item =>
            <div
              key={item}
              className={'tab ' + (item === active ? 'active' : '')}
              onClick= {() => changeActive(item)}
            >
              <span>{ item }</span>
            </div>
          )
        }
      </div>
    )
  }
}

App.js

import { Component } from 'react'

import Tabs from './Tabs'

export default class App extends Component {
  constructor() {
    super()
		
    // 只有需要在UI中使用,且数据发生改变的时候,需要重新渲染UI的数据才需要被放置到state对象中
    // 其余的数据直接使用class fields 定义成类的实例属性即可
    this.tabs = ['one', 'two', 'three']

    this.state = {
      active: 'one'
    }
  }
  render() {
    return (
      <div>
        <Tabs
          active={this.state.active}
          tabs={this.tabs}
          changeActive={active => this.changeActive(active)}
        />
        <h2>{ this.state.active }</h2>
      </div>
    )
  }

  changeActive(active) {
    this.setState({
      active
    })
  }
}

模拟slot

React没有提供Vue中的slot这样的功能,但是JSX本质上就是一个对象,所以我们可以模拟插槽来进行使用

默认插槽

App.js

import { Component } from 'react'

import Cpn from './Cpn'

export default class App extends Component {
  render() {
    return (
      <Cpn>
        {/*
          这里的内容会作为Cpn的children
          被放置到Cpn的props.children属性中
        */}
        <span>default slot</span>
      </Cpn>
    )
  }
}

Cpn.js

import { Component } from 'react'

export default class Cpn extends Component {
  render() {
    return (
      <div>
        {/*
          使用props.children 这种方式来实现插槽具有一定的限制
          如果有多个值会作为数组被传入(此时取值的时候,需要通过索引)
          如果只有一个值的时候,值就是对应的值(注意:是对应的值,不是数组)
          所以如果props.children的值是数组的时候,
          对于传入的slot的顺序会具有一定的要求
          所以使用props.children来模拟slot的时候,一般适用于只有一个slot的情况
          也就是只要缺省插槽的情况
        */}
        { this.props.children }
      </div>
    )
  }
}

具名插槽

App.js

import { Component } from 'react'

import Cpn from './Cpn'

export default class App extends Component {
  constructor() {
    super()

    this.leftSlot =  <h2>left item</h2>
    this.centerSlot =  <h2>center item</h2>
    this.rightSlot =  <h2>right item</h2>
  }
  render() {
    return (
      <Cpn
        leftSlot={ this.leftSlot }
        centerSlot={ this.centerSlot }
        rightSlot={ this.rightSlot }
      />
    )
  }
}

Cpn.js

import { Component } from 'react'

export default class Cpn extends Component {
  render() {
    return (
      <div>
        <div className="left">{ this.props.leftSlot }</div>
        <div className="center">{ this.props.centerSlot }</div>
        <div className="right">{ this.props.rightSlot }</div>
      </div>
    )
  }
}

跨父子组件通信

在实际使用的场景中,我们可能会遇到父组件和孙子组件之间需要进行通信

此时我们有多种实现方式

props逐层进行传递
import { Component } from 'react'

function ProfileHeader(props) {
  return (
    <div>
      <h2>nickname: { props.nickname }</h2>
      <h2>level: { props.level }</h2>
    </div>
  )
}

function Profile(props) {
  return (
    <div>
      <ProfileHeader {...props} />
      <h2>content</h2>
    </div>
  )
}

export default class App extends Component {
  constructor() {
    super()

    this.state = {
      nickname: 'Klaus',
      level: 6
    }
  }

  render() {
    return (
      <div>
        {/* spread attributes是jsx提供的语法糖,可以将解构出的属性直接作为组件的props进行传递 */}
        <Profile {...this.state}/>
      </div>
    )
  }
}

但是此时,中间组件Profile明显是没有使用到nickname和level这两个props的

但是因为props逐层传递的需要,Profiles依旧需要进行对应的属性传递

因为React提供了context,(类似于Vue中的provideinject

context基本使用

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props

函数组件

import React,{ Component } from 'react'

// 1. 创建context
// React.createContext()中的参数是默认值
// React.createContext参数的返回值本身是一个React组件
const userContext = React.createContext({
  nickname: 'default name',
  level: -1
})

function ProfileHeader() {
  return (
    <userContext.Consumer>
      {/*
        3. 在消费者中使用context中的数据
      */}
      {
        info => (
          <div>
            <h2>nickname: { info.nickname }</h2>
            <h2>level: { info.level }</h2>
          </div>
        )
      }
    </userContext.Consumer>
  )
}

function Profile(props) {
  return <ProfileHeader {...this.props} />
}

export default class App extends Component {
  constructor() {
    super()

    this.state = {
      nickname: 'Klaus',
      level: 6
    }
  }

  render() {
    return (
      <div>
        {/*
          2. 提供需要使用context传递的数据
             需要传递的值存放于value属性中,多个值组成为对象后在进行传递
         */}
        <userContext.Provider value={this.state}>
          <Profile />
        </userContext.Provider>
        
        <hr />

        {/* 不使用userContext.Provider包裹,则使用默认值 */}
        <Profile />
      </div>
    )
  }
}

类组件

import React, { Component } from 'react'

const userContext = React.createContext({
  nickname: 'default name',
  level: -1
})

class ProfileHeader extends Component {
  // 在类组件中有一个属性,context
  // 默认情况下,其值是空对象,因为其不知道需要使用哪个ctx中的值
  // 所以类中有一个静态属性contextType
  // 用来指定context属性,应该使用哪个ctx中提供的值
  static contextType = userContext

  render() {
    const { nickname, level } = this.context

    return (
      <div>
        <h2>nickname: { nickname }</h2>
        <h2>level: { level }</h2>
      </div>
    )
  }
}

class Profile extends Component {
  render() {
    return <ProfileHeader />
  }
}

export default class App extends Component {
  constructor() {
    super()

    this.state = {
      nickname: 'Klaus',
      level: 6
    }
  }

  render() {
    return (
      {/* 提供数据 */}
      <userContext.Provider value={this.state}>
        <Profile />
      </userContext.Provider>
    )
  }
}
多个context共存

函数组件

import React,{ Component } from 'react'

const userContext = React.createContext({
  nickname: 'default name',
  level: -1
})

const themeContext = React.createContext({
  color: 'red'
})

function ProfileHeader() {
  return (
    <userContext.Consumer>
      {
        info => (
          <themeContext.Consumer>
            {
              theme => (
                <div style={{color: theme.color}}>
                  <h2>nickname: { info.nickname }</h2>
                  <h2>level: { info.level }</h2>
                </div>
              )
            }
          </themeContext.Consumer>
        )
      }
    </userContext.Consumer>
  )
}

function Profile(props) {
  return <ProfileHeader />
}

export default class App extends Component {
  constructor() {
    super()

    this.state = {
      nickname: 'Klaus',
      level: 6
    }
  }

  render() {
    return (
      <div>
        <userContext.Provider value={this.state}>
          <themeContext.Provider value={{color: 'blue'}}>
            <Profile />
          </themeContext.Provider>
        </userContext.Provider>
      </div>
    )
  }
}

类组件

import React, { Component } from 'react'

const userContext = React.createContext({
  nickname: 'default name',
  level: -1
})

const themeContext = React.createContext({
  color: 'red'
})

class ProfileHeader extends Component {
  static contextType = userContext

  render() {
    const { nickname, level } = this.context

    return (
      <themeContext.Consumer>
        {
          theme => (
            <div style={{ color: theme.color }}>
              <h2>nickname: { nickname }</h2>
              <h2>level: { level }</h2>
            </div>
          )
        }
      </themeContext.Consumer>
    )
  }
}

class Profile extends Component {
  render() {
    return <ProfileHeader />
  }
}

export default class App extends Component {
  constructor() {
    super()

    this.state = {
      nickname: 'Klaus',
      level: 6
    }
  }

  render() {
    return (
      <userContext.Provider value={this.state}>
        <themeContext.Provider value={{ color: 'blue' }}>
          <Profile />
        </themeContext.Provider>
      </userContext.Provider>
    )
  }
}

此时,可以发现如果我们需要使用多个context进行数据共享的时候,很容易就出现层级过深的问题

此时推荐使用redux来取代context进行数据共享