Skip to content

什么是组件化开发呢?

组件化是一种分而治之的思想:

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

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

我们需要通过组件化的思想来思考整个应用程序:

  • 我们将一个完整的页面分成很多个组件;

  • 每个组件都用于实现页面的一个功能块;

  • 而每一个组件又可以进行细分;

  • 而组件本身又可以在多个地方进行复用;

React的组件化

组件化是React的核心思想,也是我们后续课程的重点,前面我们封装的App本身就是一个组件:

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

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

image-20230610125834182

组件化思想的应用:

有了组件化的思想,我们在之后的开发中就要充分的利用它。

尽可能的将页面拆分成一个个小的、可复用的组件。

这样让我们的代码更加方便组织和管理,并且扩展性也更强。

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

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

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

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

这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:

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

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

当然还有很多组件的其他概念:比如异步组件、高阶组件等,我们后续再学习。

类组件

类组件的定义有如下要求:

组件的名称是大写字符开头(无论类组件还是函数组件)

类组件需要继承自 React.Component

类组件必须实现render函数

在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义。

使用class定义一个组件:

constructor是可选的,我们通常在constructor中初始化一些数据;

this.state中维护的就是我们组件内部的数据;

render() 方法是 class 组件中唯一必须实现的方法;

render函数的返回值

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

React 元素:

通常通过 JSX 创建。

例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件;

无论是 <div /> 还是 <MyComponent /> 均为 React 元素。

数组或 fragments:使得 render 方法可以返回多个元素。

Portals:可以渲染子节点到不同的 DOM 子树中。

字符串或数值类型:它们在 DOM 中会被渲染为文本节点

布尔类型或 null:什么都不渲染。

javascript
import React from "react";


// 1.类组件
class App extends React.Component {
  constructor() {
    super()

    this.state = {
      message: "App Component"
    }
  }

  render() {
    // const { message } = this.state
    // 1.react元素: 通过jsx编写的代码就会被编译成React.createElement, 所以返回的就是一个React元素
    // return <h2>{message}</h2>

    // 2.组件或者fragments(后续学习)
    // return ["abc", "cba", "nba"]
    // return [
    //   <h1>h1元素</h1>,
    //   <h2>h2元素</h2>,
    //   <div>哈哈哈</div>
    // ]

    // 3.字符串/数字类型
    // return "Hello World"

    return true
  }
}

export default App;

函数组件

函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。

函数组件有自己的特点(当然,后面我们会讲hooks,就不一样了):

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;

  • this关键字不能指向组件实例(因为没有组件实例);

  • 没有内部状态(state);

我们来定义一个函数组件:

javascript
// 函数式组件
function App(props) {
  // 返回值: 和类组件中render函数返回的是一致
  return <h1>App Functional Component</h1>
}

export default App

在前面的学习中,我们主要讲解类组件,后面学习Hooks时,会针对函数式组件进行更多的学习。

认识生命周期

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

React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能;

生命周期和生命周期函数的关系:

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

比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;

比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;

比如卸载过程(Unmount),组件从DOM树中被移除的过程;

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

比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调;

比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调;

比如实现componentWillUnmount函数:组件即将被移除时,就会回调;

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

我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命周期的回调)

生命周期解析

我们先来学习一下最基础、最常用的生命周期函数:

image-20230610150918981

生命周期函数

Constructor

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

constructor中通常只做两件事情:

通过给 this.state 赋值对象来初始化内部的state;

为事件绑定实例(this);

componentDidMount

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

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

  • 依赖于DOM的操作可以在这里进行;

  • 在此处发送网络请求就最好的地方;(官方建议)

  • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);

componentDidUpdate

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

当组件更新后,可以在此处对 DOM 进行操作;

如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。

componentWillUnmount

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

在此方法中执行必要的清理操作;

例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;

javascript
import React from "react"

class HelloWorld extends React.Component {
  // 1.构造方法: constructor
  constructor() {
    console.log("HelloWorld constructor")
    super()

    this.state = {
      message: "Hello World"
    }
  }

  changeText() {
    this.setState({ message: "你好啊, 李银河" })
  }

  // 2.执行render函数
  render() {
    console.log("HelloWorld render")
    const { message } = this.state

    return (
      <div>
        <h2>{message}</h2>
        <p>{message}是程序员的第一个代码!</p>
        <button onClick={e => this.changeText()}>修改文本</button>
      </div>
    )
  }

  // 3.组件被渲染到DOM: 被挂载到DOM
  componentDidMount() {
    console.log("HelloWorld componentDidMount")
  }

  // 4.组件的DOM被更新完成: DOM发生更新
  componentDidUpdate() {
    console.log("HelloWorld componentDidUpdate")
  }

  // 5.组件从DOM中卸载掉: 从DOM移除掉
  componentWillUnmount() {
    console.log("HelloWorld componentWillUnmount")
  }
}

export default HelloWorld

刚开始渲染执行的生命周期顺序

javascript
HelloWorld constructor
HelloWorld render
HelloWorld componentDidMount

点击修改文本按钮,就会触发更新,执行的生命周期顺序

javascript
HelloWorld render
HelloWorld componentDidUpdate

App.jsx

javascript
import React from "react"
import HelloWorld from "./HelloWorld"

class App extends React.Component {
  constructor() {
    super()

    this.state = {
      isShowHW: true
    }
  }

  switchHWShow() {
    this.setState({ isShowHW: !this.state.isShowHW })
  }

  render() {
    const { isShowHW } = this.state

    return (
      <div>
        哈哈哈
        <button onClick={e => this.switchHWShow()}>切换</button>
        { isShowHW && <HelloWorld/> }
      </div>
    )
  }
}

export default App

点击切换按钮,组件会被卸载,执行componentWillUnmount周期函数。

不常用生命周期函数

除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:

getDerivedStateFromProps:state 的值在任何时候都依赖于 props时使用;该方法返回一个对象来更新state;

getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);

shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;

另外,React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用。

更详细的生命周期相关的内容,可以参考官网:

https://zh-hans.reactjs.org/docs/react-component.html

image-20230610152840865

javascript
import React from "react"

class HelloWorld extends React.Component {
  // 1.构造方法: constructor
  constructor() {
    console.log("HelloWorld constructor")
    super()

    this.state = {
      message: "Hello World"
    }
  }

  changeText() {
    this.setState({ message: "你好啊, 李银河" })
  }

  // 2.执行render函数
  render() {
    console.log("HelloWorld render")
    const { message } = this.state

    return (
      <div>
        <h2>{message}</h2>
        <p>{message}是程序员的第一个代码!</p>
        <button onClick={e => this.changeText()}>修改文本</button>
      </div>
    )
  }

  // 3.组件被渲染到DOM: 被挂载到DOM
  componentDidMount() {
    console.log("HelloWorld componentDidMount")
  }

  // 4.组件的DOM被更新完成: DOM发生更新
  componentDidUpdate(prevProps, prevState, snapshot) { // snapshot拿到记录的数据
    console.log("HelloWorld componentDidUpdate:", prevProps, prevState, snapshot)
  }

  // 5.组件从DOM中卸载掉: 从DOM移除掉
  componentWillUnmount() {
    console.log("HelloWorld componentWillUnmount")
  }


  // 不常用的生命周期补充
  shouldComponentUpdate() {
    return true 
  }

  getSnapshotBeforeUpdate() {
    console.log("getSnapshotBeforeUpdate")
    return {
      scrollPosition: 1000 // 这里记录数据
    }
  }
}

export default HelloWorld

点击修改文本,在render方法执行之前,会先执行shouldComponentUpdate这个方法,默认是返回true,如果改成返回false,那么就不会执行render方法,也就不会触发更新。

在componentDidUpdate函数之前有个getSnapshotBeforeUpdate方法,可以用来记录一些数据,比如滚动的位置,那么在componentDidUpdate函数的第3个参数就可以拿到记录的数据。

认识组件的嵌套

组件之间存在嵌套关系:

在之前的案例中,我们只是创建了一个组件App;

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

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

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

上面的嵌套逻辑如下,它们存在如下关系:

App组件是Header、Main、Footer组件的父组件;

Main组件是Banner、ProductList组件的父组件;

image-20230610154202817

推荐插件安装

ES7+ React/Redux/React-Native snippets,插件搜索react就能找到

App.jsx

javascript
import React, { Component } from 'react'
import Header from './c-cpns/Header'
import Footer from './c-cpns/Footer'
import Main from './c-cpns/Main'

export class App extends Component {
  render() {
    return (
      <div className='app'>
        <Header/>
        <Main/>
        <Footer/>
      </div>
    )
  }
}

export default App

c-cpns/Header.jsx

javascript
import React, { Component } from 'react'

export class Header extends Component {
  render() {
    return (
      <div>Header</div>
    )
  }
}

export default Header

c-cpns/Main.jsx

javascript
import React, { Component } from 'react'
import MainBanner from './MainBanner'
import MainProductList from './MainProductList'

export class Main extends Component {
  render() {
    return (
      <div className='main'>
        <div>Main</div>
        <MainBanner/>
        <MainProductList/>
      </div>
    )
  }
}

export default Main

c-cpns/MainBanner.jsx

javascript
import React, { Component } from 'react'

export class MainBanner extends Component {
  render() {
    return (
      <div>
        <h2>封装一个轮播图</h2>
      </div>
    )
  }
}

export default MainBanner

c-cpns/MainProduct.jsx

javascript
import React, { Component } from 'react'

export class MainProductList extends Component {
  render() {
    return (
      <div>
        <h2>商品列表</h2>
        <ul>
          <li>商品item01</li>
          <li>商品item02</li>
          <li>商品item03</li>
          <li>商品item04</li>
        </ul>
      </div>
    )
  }
}

export default MainProductList

c-cpns/Footer.jsx

javascript
import React, { Component } from 'react'

export class Footer extends Component {
  render() {
    return (
      <div>Footer</div>
    )
  }
}

export default Footer

认识组件间的通信

在开发过程中,我们会经常遇到需要组件之间相互进行通信:

比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;

又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;

也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;

总之,在一个React项目中,组件之间的通信是非常重要的环节;

父组件在展示子组件,可能会传递一些数据给子组件:

  • 父组件通过 属性=值 的形式来传递给子组件数据;

  • 子组件通过 props 参数获取父组件传递过来的数据;

Main.jsx

javascript
import React, { Component } from 'react'
import axios from "axios"

import MainBanner from './MainBanner'
import MainProductList from './MainProductList'

export class Main extends Component {
  constructor() {
    super()

    this.state = {
      banners: [],
      productList: []
    }
  }

  componentDidMount() {
    axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
      const banners = res.data.data.banner.list
      const recommend = res.data.data.recommend.list
      this.setState({
        banners,
        productList: recommend
      })
    })
  }

  render() {
    const { banners, productList } = this.state

    return (
      <div className='main'>
        <div>Main</div>
        <MainBanner banners={banners} title="轮播图"/>
        <MainBanner/>
        <MainProductList productList={productList}/>
      </div>
    )
  }
}

export default Main

MainBanner.jsx

javascript
import React, { Component } from 'react'

export class MainBanner extends Component {

  constructor(props) { // 整个constructor也可以不写
    super(props) // 内部相当于做了this.props = props的操作

    this.state = {}
  }

  render() {
    // console.log(this.props)
    const { title, banners } = this.props

    return (
      <div className='banner'>
        <h2>封装一个轮播图: {title}</h2>
        <ul>
          {
            banners.map(item => {
              return <li key={item.acm}>{item.title}</li>
            })
          }
        </ul>
      </div>
    )
  }
}

export default MainBanner

参数propTypes

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

当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证;

但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证;

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

更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

比如验证数组,并且数组中包含哪些元素;

比如验证对象,并且对象中包含哪些key以及value是什么类型;

比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired

如果没有传递,我们希望有默认值呢?

我们使用defaultProps就可以了

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

export class MainBanner extends Component {
  // 默认值也可以使用这种写法,比较新的写法
  // static defaultProps = {
  //   banners: [],
  //   title: "默认标题"
  // }

  constructor(props) {
    super(props)

    this.state = {}
  }

  render() {
    // console.log(this.props)
    const { title, banners } = this.props

    return (
      <div className='banner'>
        <h2>封装一个轮播图: {title}</h2>
        <ul>
          {
            banners.map(item => {
              return <li key={item.acm}>{item.title}</li>
            })
          }
        </ul>
      </div>
    )
  }
}

// MainBanner传入的props类型进行验证
MainBanner.propTypes = {
  banners: PropTypes.array,
  title: PropTypes.string
}

// MainBanner传入的props的默认值
MainBanner.defaultProps = {
  banners: [],
  title: "默认标题"
}

export default MainBanner

子组件传递父组件

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

在vue中是通过自定义事件来完成的;

在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;

App.jsx

javascript
import React, { Component } from 'react'
import AddCounter from './AddCounter'
import SubCounter from './SubCounter'

export 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>
        <h2>当前计数: {counter}</h2>
        <AddCounter addClick={(count) => this.changeCounter(count)}/>
        <SubCounter subClick={(count) => this.changeCounter(count)}/>
      </div>
    )
  }
}

export default App

AddCounter.jsx

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

export class AddCounter extends Component {
  addCount(count) {
    this.props.addClick(count)
  }

  render() {

    return (
      <div>
        <button onClick={e => this.addCount(1)}>+1</button>
        <button onClick={e => this.addCount(5)}>+5</button>
        <button onClick={e => this.addCount(10)}>+10</button>
      </div>
    )
  }
}

// AddCounter.propTypes = {
//   addClick: PropTypes.func
// }

export default AddCounter

SubCounter.jsx

javascript
import React, { Component } from 'react'

export class SubCounter extends Component {
  subCount(count) {
    this.props.subClick(count)
  }

  render() {
    return (
      <div>
        <button onClick={e => this.subCount(-1)}>-1</button>
        <button onClick={e => this.subCount(-5)}>-5</button>
        <button onClick={e => this.subCount(-10)}>-10</button>
      </div>
    )
  }
}

export default SubCounter

组件通信案例练习

image-20230610172134198

App.jsx

javascript
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 })
  }

  render() {
    const { titles, tabIndex } = this.state

    return (
      <div className='app'>
        <TabControl titles={titles} tabClick={i => this.tabClick(i)}/>
        <h1>{titles[tabIndex]}</h1>
      </div>
    )
  }
}

export default App

TabControl/index.jsx

javascript
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 } = 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>
              </div>
            )
          })
        }
      </div>
    )
  }
}

export default TabControl

TabControl/style.css

css
.tab-control {
  display: flex;
  align-items: center;
  height: 40px;
  text-align: center;
}

.tab-control .item {
  flex: 1;
}

.tab-control .item.active {
  color: red;
}

.tab-control .item.active .text {
  padding: 3px;
  border-bottom: 3px solid red;
}

React中的插槽(slot)

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

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

image-20230610175924267

这种需求在Vue当中有一个固定的做法是通过slot来完成的,React呢?

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

  • 组件的children子元素;
  • props属性传递React元素;

children实现插槽

每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。

如果传入多个元素,那么props.children是一个数组,但是如果只传入一个元素,那么props.children就是一个元素

App.jsx

javascript
import React, { Component } from 'react'
import NavBar from './nav-bar'

export class App extends Component {
  render() {

    return (
      <div>
        {/* 1.使用children实现插槽 */}
        <NavBar>
          <button>按钮</button>
          <h2>哈哈哈</h2>
          <i>斜体文本</i>
        </NavBar>
      </div>
    )
  }
}

export default App

nav-bar/index.jsx

javascript
import React, { Component } from 'react'
// import PropTypes from "prop-types"
import "./style.css"

export class NavBar extends Component {
  render() {
    const { children } = this.props
    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>
    )
  }
}

// NavBar.propTypes = {
//   children: PropTypes.array
// }

export default NavBar

props实现插槽

通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;

另外一种方案就是使用 props 实现:

通过具体的属性名,可以让我们在传入和获取时更加的精准;

App.jsx

javascript
import React, { Component } from 'react'
import NavBarTwo from './nav-bar-two'

export class App extends Component {
  render() {
    const btn = <button>按钮2</button>

    return (
      <div>
        {/* 2.使用props实现插槽 */}
        <NavBarTwo 
          leftSlot={btn}
          centerSlot={<h2>呵呵呵</h2>}
          rightSlot={<i>斜体2</i>}
        />
      </div>
    )
  }
}

export default App

nav-bar-two/index.jsx

javascript
import React, { Component } from 'react'

export class NavBarTwo extends Component {
  render() {
    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>
    )
  }
}

export default NavBarTwo

作用域插槽的实现方案

还是使用上面的组件通信案例,只不过这次,切换点击的区域展示的内容不一样。

image-20230610181316188

App.jsx

javascript
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 => <button>{item}</button>}
          itemType={item => this.getTabItem(item)}
        />
        <h1>{titles[tabIndex]}</h1>
      </div>
    )
  }
}

export default App

TabControl/index.jsx

javascript
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传出去 */}
                {itemType(item)}
              </div>
            )
          })
        }
      </div>
    )
  }
}

export default TabControl

Context应用场景

非父子组件数据的共享:

在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递。

但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。

如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

我们实现一个一层层传递的案例:

我这边顺便补充一个小的知识点:Spread Attributes

javascript
import React, { Component } from 'react'
import Home from './Home'

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

    this.state = {
      info: { name: "kobe", age: 30 }
    }
  }

  render() {
    const { info } = this.state

    return (
      <div>
        <h2>App</h2>
        {/* 1.给Home传递数据 */}
        <Home name="why" age={18}/>
        <Home name={info.name} age={info.age}/>
        <Home {...info}/>

      </div>
    )
  }
}

export default App

但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

React提供了一个API:Context;类似于Vue的Provide和Inject

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

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;

Context相关API

React.createContext

创建一个需要共享的Context对象:

如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;

defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值

javascript
const MyContext = React.createContext(defaultValue)

Context.Provider

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:

Provider 接收一个 value 属性,传递给消费组件;

一个 Provider 可以和多个消费组件有对应关系;

多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;

javascript
<MyContext.Provider value={/*某个值*/}></MyContext.Provider>

Class.contextType

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:

这能让你使用 this.context 来消费最近 Context 上的那个值;

你可以在任何生命周期中访问到它,包括 render 函数中;

javascript
MyContext.contextType = MyContext

Context.Consumer

这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。

这里需要 函数作为子元素(function as child)这种做法;

这个函数接收当前的 context 值,返回一个 React 节点;

Context代码演练

Context的基本使用

什么时候使用默认值defaultValue呢?

Home组件没有包裹在UserContext.Provider中

image-20230610203151802

什么时候使用Context.Consumer呢?

1.当使用value的组件是一个函数式组件时;

2.当组件中需要使用多个Context时;

App.jsx

javascript
import React, { Component } from 'react'
import Home from './Home'

import ThemeContext from "./context/theme-context"
import UserContext from './context/user-context'
import Profile from './Profile'

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

    this.state = {
      info: { name: "kobe", age: 30 }
    }
  }

  render() {
    const { info } = this.state

    return (
      <div>
        <h2>App</h2>
        {/* 1.给Home传递数据 */}
        {/* <Home name="why" age={18}/>
        <Home name={info.name} age={info.age}/>
        <Home {...info}/> */}

        {/* 2.普通的Home */}
        {/* 第二步操作: 通过ThemeContext中Provider中value属性为后代提供数据 */}
        <UserContext.Provider value={{nickname: "kobe", age: 30}}>
          <ThemeContext.Provider value={{color: "red", size: "30"}}>
            <Home {...info}/>
          </ThemeContext.Provider>
        </UserContext.Provider>
		{/*使用context默认值,没有被context包裹*/}
        <Profile/> 
      </div>
    )
  }
}

export default App

context/theme-context.js

javascript
import React from "react"

// 1.创建一个Context
const ThemeContext = React.createContext({ color: "blue", size: 10 })

export default ThemeContext

context/user-context.js

javascript
import React from "react"

// 1.创建一个Context
const UserContext = React.createContext()

export default UserContext

Home.jsx

javascript
import React, { Component } from 'react'
import HomeBanner from './HomeBanner'
import HomeInfo from './HomeInfo'

export class Home extends Component {
  render() {
    const { name, age } = this.props

    return (
      <div>
        <h2>Home: {name}-{age}</h2>
        <HomeInfo/>
        <HomeBanner/>
      </div>
    )
  }
}

export default Home

HomeBanner.jsx

javascript
import ThemeContext from "./context/theme-context"

function HomeBanner() {

  return <div>
    {/* 函数式组件中使用Context共享的数据 */}
    <ThemeContext.Consumer>
      {
        value => {
          return <h2> Banner theme:{value.color}</h2>
        }
      }
    </ThemeContext.Consumer>
  </div>
}

export default HomeBanner

HomeInfo.jsx

javascript
import React, { Component } from 'react'
import ThemeContext from './context/theme-context'
import UserContext from './context/user-context'

export class HomeInfo extends Component {
  render() {
    // 4.第四步操作: 获取数据, 并且使用数据
    console.log(this.context)

    return (
      <div>
        <h2>HomeInfo: {this.context.color}</h2>
		{/*多个context使用Consumer*/}
        <UserContext.Consumer>
          {
            value => {
              return <h2>Info User: {value.nickname}</h2>
            }
          }
        </UserContext.Consumer>
      </div>
    )
  }
}

// 3.第三步操作: 设置组件的contextType为某一个Context,只能设置一个context
HomeInfo.contextType = ThemeContext

export default HomeInfo

Profile.jsx

javascript
import React, { Component } from 'react'
import ThemeContext from './context/theme-context'

export class Profile extends Component {
  render() {
    console.log(this.context)

    return (
      <div>Profile</div>
    )
  }
}

Profile.contextType = ThemeContext

export default Profile

非父子组件通信-事件总线

utlis/event-bus.js

javascript
import { HYEventBus } from "hy-event-store"

const eventBus = new HYEventBus()

export default eventBus

App.jsx

javascript
import React, { Component } from 'react'
import Home from './Home'
import eventBus from './utils/event-bus'

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

    this.state = {
      name: "",
      age: 0,
      height: 0
    }
  }

  componentDidMount() {
    // eventBus.on("bannerPrev", (name, age, height) => {
    //   console.log("app中监听到bannerPrev", name, age, height)
    //   this.setState({ name, age, height })
    // })

    eventBus.on("bannerPrev", this.bannerPrevClick, this) // 这里要绑定this
    eventBus.on("bannerNext", this.bannerNextClick, this)
  }

  bannerPrevClick(name, age, height) {
    console.log("app中监听到bannerPrev", name, age, height)
    this.setState({ name, age, height })
  }

  bannerNextClick(info) {
    console.log("app中监听到bannerNext", info)
  }

  componentWillUnmount() {
    eventBus.off("bannerPrev", this.bannerPrevClick, this)
    eventBus.off("bannerNext", this.bannerNextClick, this)
  }

  render() {
    const { name, age, height } = this.state

    return (
      <div>
        <h2>App Component: {name}-{age}-{height}</h2>
        <Home/>
      </div>
    )
  }
}

export default App

上面事件总线监听函数的this绑定除了上面的写法还有下面2种方法:

javascript
componentDidMount() {
    eventBus.on("bannerPrev", this.bannerPrevClick.bind(this))
}

bannerPrevClick() {
    ...
}

也可以使用箭头函数

javascript
componentDidMount() {
    eventBus.on("bannerPrev", this.bannerPrevClick)
}

bannerPrevClick = () => {
    ...
}

HomeBanner.jsx

javascript
import React, { Component } from 'react'
import eventBus from "./utils/event-bus"

export class HomeBanner extends Component {
  prevClick() {
    console.log("上一个")

    eventBus.emit("bannerPrev", "why", 18, 1.88)
  }

  nextClick() {
    console.log("下一个")
    eventBus.emit("bannerNext", {nickname: "kobe", level: 99})
  }

  render() {
    return (
      <div>
        <h2>HomeBanner</h2>
        <button onClick={e => this.prevClick()}>上一个</button>
        <button onClick={e => this.nextClick()}>下一个</button>
      </div>
    )
  }
}

export default HomeBanner

为什么使用setState

开发中我们并不能直接通过修改state的值来让界面发生更新:

因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;

React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;

我们必须通过setState来告知React数据已经发生了变化;

疑惑:在组件中并没有实现setState的方法,为什么可以调用呢?

原因很简单,setState方法是从Component中继承过来的。

image-20230611143946361

setState异步更新

setState的更新是异步的?

javascript
changeText() {
    this.setState({ message: "你好啊, 李银河" })
	console.log("------:", this.state.message)
}

最终打印结果是Hello World;

可见setState是异步的操作,我们并不能在执行完setState之后立马拿到最新的state的结果

javascript
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App name="why"/>
);
javascript
import React, { Component } from 'react'

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

    this.state = {
      message: "Hello World",
      counter: 0
    }
  }

  changeText() {
    // 1.setState更多用法
    // 1.基本使用,setState内部做的事情是使用Object.assign覆盖掉原先的state的属性
    // this.setState({
    //   message: "你好啊, 李银河"
    // })

    // 2.setState可以传入一个回调函数
    // 好处一: 可以在回调函数中编写新的state的逻辑
    // 好处二: 当前的回调函数会将之前的state和props传递进来
    // this.setState((state, props) => {
    //   // 1.编写一些对新的state处理逻辑
    //   // 2.可以获取之前的state和props值
    //   console.log(this.state.message, this.props)

    //   return {
    //     message: "你好啊, 李银河"
    //   }
    // })

    // 3.setState在React的事件处理中是一个异步调用
    // 如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码
    // 那么可以在setState中传入第二个参数: callback
    this.setState({ message: "你好啊, 李银河" }, () => {
      console.log("++++++:", this.state.message)
    })
    console.log("------:", this.state.message)
  }

  increment() {

  }

  render() {
    const { message, counter } = this.state

    return (
      <div>
        <h2>message: {message}</h2>
        <button onClick={e => this.changeText()}>修改文本</button>
        <h2>当前计数: {counter}</h2>
        <button onClick={e => this.increment()}>counter+1</button>
      </div>
    )
  }
}

export default App

为什么setState设计为异步呢?

setState设计为异步其实之前在GitHub上也有很多的讨论;

React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的同学可以参考一下;

https://github.com/facebook/react/issues/11527#issuecomment-360199710;

我对其回答做一个简单的总结:

setState设计为异步,可以显著的提升性能;

如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;

最好的办法应该是获取到多个更新,之后进行批量更新;

如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;

state和props不能保持一致性,会在开发中产生很多的问题;

javascript
import React, { Component } from 'react'

function Hello(props) {
  return <h2>{props.message}</h2>
}

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

    this.state = {
      message: "Hello World",
      counter: 0
    }
  }

  componentDidMount() {
    // 1.网络请求一: banners

    // 2.网络请求二: recommends

    // 3.网络请求三: productlist
  }

  changeText() {
    this.setState({ message: "你好啊,李银河" })
    // 如果是同步的,message变成了 你好啊,李银河,但是还没来得及执行render,那么子组件获取的message还是原来的Hello World
    console.log(this.state.message) 
  }

  increment() {
    console.log("------")
    // 这种写法会进行合并,执行了3次,counter还是1
    // this.setState({
    //   counter: this.state.counter + 1
    // })
    // this.setState({
    //   counter: this.state.counter + 1
    // })
    // this.setState({
    //   counter: this.state.counter + 1
    // })
	
    // 下面这种写法counter会变成3
    // this.setState((state) => { // 这里面的state拿到的是上一次的state
    //   return {
    //     counter: state.counter + 1
    //   }
    // })
    // this.setState((state) => {
    //   return {
    //     counter: state.counter + 1
    //   }
    // })
    // this.setState((state) => {
    //   return {
    //     counter: state.counter + 1
    //   }
    // })
  }

  render() {
    const { message, counter } = this.state
    console.log("render被执行")

    return (
      <div>
        <h2>message: {message}</h2>
        <button onClick={e => this.changeText()}>修改文本</button>
        <h2>当前计数: {counter}</h2>
        <button onClick={e => this.increment()}>counter+1</button>

        <Hello message={message}/>
      </div>
    )
  }
}

export default App

如何获取异步的结果

那么如何可以获取到更新后的值呢?

方式一:setState的回调

setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;

格式如下:setState(partialState, callback)

image-20230611152713596

当然,我们也可以在生命周期函数:

image-20230611152733166

setState一定是异步吗?(React18之前)

验证一:在setTimeout中的更新:

image-20230611152808288

验证二:原生DOM事件:

image-20230611152918145

其实分成两种情况:

在组件生命周期或React合成事件(就是调用方法)中,setState是异步;

在setTimeout或者原生dom事件中,setState是同步;

setState默认是异步的(React18之后)

在React18之后,默认所有的操作都被放到了批处理中(异步处理)。

image-20230611153047076

如果希望代码可以同步会拿到,则需要执行特殊的flushSync操作:

image-20230611153107611
javascript
import React, { Component } from 'react'
import { flushSync } from 'react-dom'

function Hello(props) {
  return <h2>{props.message}</h2>
}

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

    this.state = {
      message: "Hello World",
      counter: 0
    }
  }

  componentDidMount() {
    // 1.网络请求一: banners

    // 2.网络请求二: recommends

    // 3.网络请求三: productlist
  }

  changeText() {
    setTimeout(() => {
      // 在react18之前, setTimeout中setState操作, 是同步操作
      // 在react18之后, setTimeout中setState异步操作(批处理)
      flushSync(() => {
        this.setState({ message: "你好啊, 李银河" })
      })
      // react18之后,如果更新了state之后想立马获取到值,可以使用flushSync
      console.log(this.state.message)
    }, 0);
  }

  increment() {
  }

  render() {
    const { message, counter } = this.state
    console.log("render被执行")

    return (
      <div>
        <h2>message: {message}</h2>
        <button onClick={e => this.changeText()}>修改文本</button>
        <h2>当前计数: {counter}</h2>
        <button onClick={e => this.increment()}>counter+1</button>

        <Hello message={message}/>
      </div>
    )
  }
}

export default App