likes
comments
collection
share

React 组件化开发基础

作者站长头像
站长
· 阅读数 53

一、React的组件化

1.1 组件化思想

  • 组件化是一种分而治之的思想:
    • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
    • 但如果将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了

1.2 React的组件化

  • React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
    • 根据组件的定义方式,可以分为:函数组件(Functional Component )类组件(Class Component)
    • 根据组件内部是否有状态需要维护,可以分为:无状态组件(Stateless Component )有状态组件(Stateful Component)
    • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)容器型组件(Container Component)
  • 这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:
    • 函数组件、无状态组件、展示型组件主要关注UI的展示
    • 类组件、有状态组件、容器型组件主要关注数据逻辑

二、类组件和函数组件

2.1 类组件

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

    • 组件的名称是大写字符开头(无论类组件还是函数组件)
    • 类组件需要继承自 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:什么都不渲染
import { Component } from "react";

class App extends 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"
  }
}

export default App;

2.2 函数式组件

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

  • 函数组件有自己的特点(暂未用hooks):

    • 没有生命周期,也会被更新并挂载,但是没有生命周期函数
    • this关键字不能指向组件实例(因为没有组件实例)
    • 没有内部状态(state)
function App() {
  // 返回值:和类组件中render函数的返回值一致
  return <h2>App Functional Component</h2>
}

export default App;

三、组件的生命周期

3.1 认识生命周期

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

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

    • 装载阶段(Mount),组件第一次在DOM树中被渲染的过程
    • 更新过程(Update),组件状态发生变化,重新更新渲染的过程
    • 卸载过程(Unmount),组件从DOM树中被移除的过程
  • React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:

    • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调
    • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调
    • 比如实现componentWillUnmount函数:组件即将被移除时,就会回调
  • 注意:这里谈到React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的(可以通过hooks来模拟一些生命周期的回调,后续补充)

3.2 生命周期解析

  • 执行顺序 React 组件化开发基础

    • 通过类创建实例,首先会执行constructor,之后执行render渲染,组件被挂载到DOM
    • constructor -> render -> componentDidMount
  • constructor

    • 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
    • constructor中通常只做两件事情:
      • 通过给 this.state 赋值对象来初始化内部的state
      • 为事件绑定实例(this)
  • componentDidMount

    • componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用
    • componentDidMount中通常进行哪里操作呢?
      • 依赖于DOM的操作可以在这里进行
      • 在此处发送网络请求就最好的地方(官方建议)
      • 可以在此处添加一些订阅(需要在componentWillUnmount取消订阅)
  • componentDidUpdate

    • componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法
      • 当组件更新后,可以在此处对 DOM 进行操作
    • 如果对更新前后的 props 进行了比较,也可以选择在此处进行网络请求(例如,当 props 未发生变化时,则不会执行网络请求)
  • componentWillUnmount

    • componentWillUnmount() 会在组件卸载及销毁之前直接调用
    • 在此方法中执行必要的清理操作;
      • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅
  • 代码演示

    • 父组件
    import React from "react"
    
    import HelloWorld from "./HelloWorld"
    
    class App extends React.Component {
      constructor() {
        super()
    
        this.state = {
          isShowHW: true
        }
      }
    
      switchHW() {
        this.setState({ isShowHW: !this.state.isShowHW })
      }
    
      render() {
        const { isShowHW } = this.state
    
        return (
          <div>
            哈哈哈
            <button onClick={() => this.switchHW()}>切换</button>
            {isShowHW && <HelloWorld />}
          </div>
        )
      }
    }
    
    export default App
    
    • 子组件
    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: "Hello React!" })
      }
    
      // 2.执行render函数
      render() {
        console.log("HelloWorld render");
    
        const { message } = this.state
    
        return (
          <div>
            <h2>{message}</h2>
            <p>{message}是第一行代码!</p>
            <button onClick={() => this.changeText()}>修改文本</button>
          </div>
        )
      }
    
      // 3.组件的DOM被挂载
      componentDidMount() {
        console.log("HelloWorld componentDidMount")
      }
    
      // 4.组件的DOM更新
      componentDidUpdate() {
        console.log("HelloWorld componentDidUpdate")
      }
    
      // 5.组件即将从DOM中移除
      componentWillUnmount() {
        console.log("HelloWorld componentWillUnmount");
      }
    }
    
    export default HelloWorld
    
    • 执行结果

    React 组件化开发基础

3.3 不常用生命周期函数

React 组件化开发基础

  • getDerivedStateFromProps:

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

    • 在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置)
  • shouldComponentUpdate:该生命周期函数很常用,后续补充

四、组件通信

组件嵌套关系

React 组件化开发基础

4.1 父子组件的通信

4.1.1 父传子 props

  • 通信方式

    • 父组件通过 属性=值 的形式来传递给子组件数据
    • 子组件通过 props 参数获取父组件传递过来的数据
  • 参数 propTypes:限制传递数据的类型,进行类型检查

    • 引入类型库:import propTypes from 'prop-types'
    • 使用:
      Banner.propTypes = {
        banners: propTypes.array.isRequired,
        title: propTypes.string
      }
      
    • 更多写法:官网
  • 传递参数默认值:defaultProps

    • 常用写法:
      Banner.defaultProps = {
        banners: [],
        title: "默认标题"
      }
      
    • ES2022 支持的写法:写入 类组件内部
      static defaultProps = {
          banners: [],
          title: "默认标题"
        }
      
  • Main组件

    import React, { Component } from 'react'
    
    import axios from 'axios'
    
    import MainBanner from './Banner'
    import MainProductList from './ProductList'
    
    export default 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 recommends = res.data.data.recommend.list
          this.setState({
            banners,
            productList: recommends
          })
        })
      }
    
      render() {
    
        const { banners, productList } = this.state
    
        return (
          <div className='main'>
            <MainBanner banners={banners} title="轮播图" />
            <MainProductList productList={productList} />
          </div>
        )
      }
    }
    
  • Banner 组件

    import React, { Component } from 'react'
    import propTypes from 'prop-types'
    
    export class Banner extends Component {
      // ES2022 新特性
      // static defaultProps = {
      //   banners: [],
      //   title: "默认标题"
      // }
    
      // constructor(props) {
      //   super(props)
      // }
    
      render() {
        const { title, banners } = this.props
        return (
          <div>
            <h2>封装轮播图:{title}</h2>
            <ul>
              {
                banners.map(banner => <li key={banner.acm}>{banner.title}</li>)
              }
            </ul>
          </div>
        )
      }
    }
    
    Banner.propTypes = {
      // banners: propTypes.array.isRequired,
      banners: propTypes.array,
      title: propTypes.string
    }
    
    Banner.defaultProps = {
      banners: [],
      title: "默认标题"
    }
    
    export default Banner
    

4.1.2 子传父 props => 函数

  • 回调函数
    • 子组件通过调用父组件传递过来的函数来实现通信

App组件放一个数字,通过AddCounter组件的按钮来对数字进行加法运算

React 组件化开发基础

  • App组件

    import React, { Component } from 'react'
    
    import AddCounter from './AddCounter'
    
    export default class App extends Component {
    
      constructor() {
        super()
    
        this.state = {
          counter: 100
        }
      }
    
      changeCount(count) {
        this.setState({ counter: this.state.counter + count })
      }
    
      render() {
    
        const { counter } = this.state
    
        return (
          <div>
            <h2>{counter}</h2>
            <AddCounter addClick={count => this.changeCount(count)} />
          </div>
        )
      }
    }
    
  • AddCounter 组件

    import React, { Component } from 'react'
    
    export default 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>
        )
      }
    }
    

4.1.3 父子通信的案例练习

  • 效果

    React 组件化开发基础

  • 代码

    • App.jsx
    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)}/>
            <h2>{titles[tabIndex]}</h2>
          </div>
        )
      }
    }
    
    export default App
    
    • TabControl/index.jsx
    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
    

4.2 非父子组件的通信

4.2.1 spread props/attributes

  • 通过props属性自上而下(由父到子到孙)进行传递

  • 在给组件传递数据时,可以通过 {...this.props} 这样的方式依次向下传递,见官网

    import React, { Component } from 'react'
    
    import Home from './Home'
    
    export default class App extends Component {
    
      constructor() {
        super()
    
        this.state = {
          info: { name: "韩梅梅", age: 17 }
        }
      }
    
      render() {
        const { info } = this.state
        return (
          <div>
            <h2>App</h2>
            <Home name={"李雷"} age={18} />
            <Home {...info} />
          </div>
        )
      }
    }
    

4.2.2 Context基本使用

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

    • React提供了一个API:Context
    • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
    • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
  • context 使用步骤

    1. 创建Context
    2. Context.Provider提供value
    3. 类组件 contextType = Context
    4. 类组件 render() 中获取 this.context
  • React.createContext:const MyContext = React.createContext(defaultValue)

    • 创建一个需要共享的Context对象
    • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值
    • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
  • Context.Provider:<MyContext.Provider value={}>

    • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化
    • Provider 接收一个 value 属性,传递给消费组件
    • 一个 Provider 可以和多个消费组件有对应关系
    • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据
    • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染
  • Class.contextType:MyClass.contextType = MyContext

    • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象
    • 这能让你使用 this.context 来消费最近 Context 上的那个值
    • 你可以在任何生命周期中访问到它,包括 render 函数中
  • Context.Consumer:<MyContext.Consumer>{ value => /* 基于 context 值渲染 */ }</MyContext.Consumer>

    • React 组件可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context
    • 这里需要 函数作为子元素(function as child)这种做法
    • 这个函数接收当前的 context 值,返回一个 React 节点
  • 示例

    • 创建一个 themeContext
    import React from 'react'
    
    // 1.创建context
    const themeContext = React.createContext()
    
    export default themeContext
    
    • 在App.jsx 中使用
    import React, { Component } from 'react'
    import Home from './Home'
    import ThemeContext from './context/theme-context'
    
    export default class App extends Component {
    
      constructor() {
        super()
    
        this.state = {
          info: { name: "韩梅梅", age: 17 }
        }
      }
    
      render() {
        const { info } = this.state
        return (
          <div>
            <h2>App</h2>
    
            {/* 2.通过ThemeContext中Provider中value属性为后代提供数据 */}
            <ThemeContext.Provider value={{ color: 'red', size: 20 }}>
              <Home {...info} />
            </ThemeContext.Provider>
          </div>
        )
      }
    }
    
    • 在HomeInfo中使用
    import React, { Component } from 'react'
    import ThemeContext from './context/theme-context'
    
    class HomeInfo extends Component {
      render() {
        // 4.获取数据, 并且使用数据
        console.log(this.context)
    
        return (
          <div>
            <h2>HomeInfo: {this.context.color}</h2>
          </div>
        )
      }
    }
    
    // 3.设置组件的contextType为某一个Context
    HomeInfo.contextType = ThemeContext
    
    export default HomeInfo
    

4.2.3 Context额外补充

  • 在函数式组件中使用Context

    • Context.Consumer

      • value => {}
      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
      
  • 多个Context共享数据

    • 类组件Context.Consumer
      1. 当使用value的组件是一个函数式组件时
      2. 当组件中需要使用多个Context时
  • defaultValue

    • 如果使用Context, 该组件不是Context.Provider的后代组件,那么使用的defaultValue值

4.4 eventBus(mitt)

  • 在 Vue 中会通过 eventBus 进行通信,React也能够使用,此处以 第三方库 mitt 为例演示

    • 在 homeBanner 组件中发出事件,在 App 组件中 监听
  • 使用过程

    • 引入mitt,创建一个实例
    // eventBus.js
    import mitt from 'mitt'
    
    const eventBus = mitt()
    
    export default eventBus
    
    • 在 HomeBanner 组件发出事件
    import React, { Component } from 'react'
    
    import eventBus from './utils/event-bus'
    
    export default class HomeBanner extends Component {
    
      prevClick() {
        console.log('上一个')
        eventBus.emit('bannerPrev', 'prev')
      }
    
      nextClick() {
        console.log('下一个')
        eventBus.emit('bannerNext', 'next')
      }
    
      render() {
        return (
          <div>
            <h2>HomeBanner</h2>
            <button onClick={() => this.prevClick()}>上一个</button>
            <button onClick={() => this.nextClick()}>下一个</button>
          </div>
        )
      }
    }
    
    • 在 App 组件监听事件
      • 注意在组件销毁时移除监听
    import React, { Component } from 'react'
    import Home from './Home'
    
    import eventBus from './utils/event-bus'
    
    export default class App extends Component {
    
      constructor() {
        super()
    
        this.state = {
          type: 'normal'
        }
      }
    
      bannerPrevClick(type) {
        console.log('App中监听到的数据', type);
        this.setState({ type })
      }
    
      bannerNextClick(type) {
        console.log('App中监听到的数据', type);
        this.setState({ type })
      }
    
      componentDidMount() {
        eventBus.on('bannerPrev', this.bannerPrevClick.bind(this))
        eventBus.on('bannerNext', this.bannerNextClick.bind(this))
      }
    
      componentWillUnmount() {
        eventBus.off('bannerPrev', this.bannerPrevClick.bind(this))
        eventBus.off('bannerNext', this.bannerNextClick.bind(this))
      }
    
      render() {
        const { type } = this.state
        return (
          <div>
            <h2>App Component: {type}</h2>
            <Home/>
          </div>
        )
      }
    }
    

五、React插槽效果实现

5.1 children方案实现

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

  • 注意:组件中传入多个子元素时是一个数组;传入一个子元素时,子组件获取的是一个react处理的 element

    • 如果需要传入一个子元素,可以在组件内部对其类型进行限制
    xxx.propTypes = {
      children: PropTypes.element
    }
    
  • 弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生

  • App.jsx

    import React, { Component } from 'react'
    
    import NavBar from './nav-bar'
    
    export default class App extends Component {
      render() {
        return (
          <div>
            <NavBar>
              <button>返回</button>
              <h2>标题</h2>
              <i>菜单</i>
            </NavBar>
          </div>
        )
      }
    }
    
  • NavBar.jsx

    import React, { Component } from 'react'
    
    import './style.css'
    
    export default class NavBar extends Component {
      render() {
    
        const { children } = this.props
    
        return (
          <div className='nav-bar'>
            <div className="left">{children[0]}</div>
            <div className="center">{children[1]}</div>
            <div className="right">{children[2]}</div>
          </div>
        )
      }
    }
    

5.2 props属性实现(推荐)

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

    React 组件化开发基础

  • App.jsx

    import React, { Component } from 'react'
    
    import NavBar from './nav-bar'
    
    export default class App extends Component {
      render() {
        return (
          <div>
            {/* 使用props */}
            <NavBar
              leftSlot={<button>按钮2</button>}
              centerSlot={<h2>呵呵呵</h2>}
              rightSlot={<i>菜单2</i>}
            />
          </div>
        )
      }
    }
    
  • NavBar.jsx

    import React, { Component } from 'react'
    
    export default class NavBar 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>
        )
      }
    }
    

5.3 作用域插槽

  • 我们都知道 Vue 中有 作用域插槽这样的概念,React中没有这样的概念,但是 React 真的太灵活了, 我们可以通过函数回调自己实现一个

  • 拿上面的父子通信的案例 来说,现在想通过在父组件传入不同元素来进行展示

    • 让父组件传入一个itemType的props 先固定写死为 <button>按钮</button>,替换子组件中的 <span>,这时会发现子组件中的tab会替换
    • 如果传入一个函数呢?
      • 在子组件中调用,传入对应的数据,就实现了作用于插槽
      • 在这个函数中处理返回给子组件的元素
  • App.jsx

    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={<button>哈哈哈</button>}
              itemType={item => this.getTabItem(item)}
            />
            <h2>{titles[tabIndex]}</h2>
          </div>
        )
      }
    }
    
    export default App
    
  • TabControl.jsx

    import React, { Component } from 'react'
    import "./style.css"
    
    export class TabControl extends Component {
      constructor() {
        super()
    
        this.state = {
          currentIndex: 0
        }
      }
    
      itemClick(index) {
        this.setState({ currentIndex: index })
    
        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)}
                  </div>
                )
              })
            }
          </div>
        )
      }
    }
    
    export default TabControl
    

六、setState究竟是同步还是异步?

6.1 为什么使用setState?

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

    • 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化
    • React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化(Vue实际是对数据进行劫持
    • 我们必须通过setState来告知React数据已经发生了变化,重新渲染
  • 疑惑:在组件中并没有实现setState的方法,为什么可以调用呢?

    • setState方法是从Component中继承过来的

      React 组件化开发基础

6.2 setState 的三种写法

  • 基本使用

    • 这种写法,在内部是通过 Object.assign() 来合并
    this.setState({
      message: "你好啊, 师姐"
    })
    
  • 传入一个回调函数,但是必须返回一个对象

    • 好处一:可以在回调函数中编写新的state的逻辑
    • 好处二:当前的回调函数会将之前的state和props传递进来
    this.setState((state, props) => {
      // 1.编写一些对新的state处理逻辑
      // 2.可以获取之前的state和props值
      console.log(this.state.message, this.props)
    
      return {
        message: "你好啊, 师姐"
      }
    })
    
  • setState在React的事件处理中是一个异步调用

    • 如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码,那么可以在setState中传入第二个参数: callback
    this.setState({ message: "你好啊, 师姐" }, () => {
      console.log("++++++:", this.state.message) // 你好啊, 师姐
    })
    console.log("------:", this.state.message) // Hello World
    

6.3 setState为什么是一个异步调用?

  • 上面的第三种写法先打印的是 Hello World,可见setState是异步的操作,我们并不能在执行完 setState 之后立马拿到最新的 state 结果

  • 为什么是异步?

  • 简单总结

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

      • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
      • 最好的办法应该是获取到多个更新之后进行批量更新
    • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步

      • state和props不能保持一致性,会在开发中产生很多的问题
    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: "你好啊,师姐" })
        console.log(this.state.message)
      }
    
      increment() {
        console.log("------")
    
        // 如果是同步,此处调用setState,会调用 3 次render函数
        // 获取多个更新放入队列中,批量更新,仅执行一次render函数
        // 注意:下面这种写法,批量更新后结果 为 1,因为,每次取到的 this.state.counter 都是0,然后通过Object.assign合并
        // this.setState({
        //   counter: this.state.counter + 1
        // })
        // this.setState({
        //   counter: this.state.counter + 1
        // })
        // this.setState({
        //   counter: this.state.counter + 1
        // })
    
        // 下面的写法,执行后结果为3,注意在回调函数中 使用 this.stats.counter 拿到的是 更新前的值
        // this.setState((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>
    
            {/* 如果是同步,再修改message后,render函数未执行(正常情况下,修改message后会有别的逻辑代码并不能保证立即执行render函数),此处的message仍为之前的值,可能会出现数据不一致的情况 */}
            <Hello message={message}/>
          </div>
        )
      }
    }
    
    export default App
    

6.4 setState 一定是异步吗?

  • React 18 之前

    • 在组件生命周期或React合成事件中,setState是异步

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

      • setTimeout中的更新
        changeText() {
          setTimeout(() => {
            // 在react18之前, setTimeout中setState操作, 是同步操作
            // 在react18之后, setTimeout中setState异步操作(批处理)
            this.setState({ message: "你好啊, 师姐" })
            console.log(this.state.message) // 你好啊, 师姐
          }, 0);
        }
        
      • 原生 DOM 事件
        componentDidMount() {
          const btnEl = document.getElementById("btn")
          btnEl.addEventListener('click', () => {
            this.setState({ message: "你好啊, 师姐" })
            console.log(this.state.message) // 你好啊, 师姐
          })
        }
        
  • React 18 之后

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

    • 官网

      React 组件化开发基础

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

      changeText() {
        setTimeout(() => {
          flushSync(() => {
            this.setState({ message: "你好啊, 师姐" })
          })
          console.log(this.state.message) // 你好啊, 师姐
        }, 0);
      }
      
转载自:https://juejin.cn/post/7144155299253846030
评论
请登录