组件化开发及setState原理和性能优化

95 阅读7分钟

1. 类组件和函数组件

1.1 类组件

import React from "react"

// 1. 类组件
class App extends React.Component {
  constructor() {
    super()
    this.state = {
      message: "Hello React"
    }
  }

  render() {
    const { message } = this.state
    return <div>{message}</div>
  }
}

export default App

1.2 函数组件

function App() {
  return <h2>Hello React</h2>
}

export default App

  • 函数组件和类组件最主要的区别是函数组价中没有this,也没有生命周期。

2. 组件的声明周期

2.1 三个周期钩子函数

  • 最常用的三个钩子函数如下:
...
componentDidMount() {
    console.log("HelloWorld componentDidMount")
  }
  componentDidUpdate() {
    console.log("HelloWorld componentDidUpdate")
  }
  componentWillUnmount() {
    console.log("HelloWorld componentWillUnmount")
  }
  ...
  • 第一个函数执行的时机为:当组件已经挂载到DOM上时,就会被回调;
  • 第二个:组件更新的时候;
  • 第三个:组件即将被移除时;

2.2 声明周期图谱

image.png

  • Constructor

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

    • componentDidMount() 会在组件挂载后(插入 DOM 树中)立即被调用
    • 在此钩子中可以执行的操作:
      • 依赖于 DOM 的操作可以在这里执行;
      • 在此处发送网络请求;
      • 可以在此添加一些订阅(需要在 componentWillUnmounted 取消订阅)
  • componentDidUpdate

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

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

3. 组件通信

3.1 父传子

  • 使用 props 传参
// Main
this.state = {
  banner: [123, 32, 23]
}
<MainBanner banner={banner} title="123" />

// MainBanner
render() {
    const { title, banner } = this.props
    return (
      <div>
        <h2>title: {title}</h2>
        <ul>
          {banner.map(item => (
            <li key={item.acm}>{item.title}</li>
          ))}
        </ul>
      </div>
    )
  }
  
// 如果在传参的时候需要验证类型,需要写如下代码:
import PropTypes from "prop-types"
MainBanner.propTypes = {
  banner: PropTypes.array.isRequired,
  title: PropTypes.string
}

3.2 子传父

// AddCounter
import React, { Component } from "react"

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

export default AddCounter

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

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

export default App

3.3 非父子组件通信-context

  • 方式一
// App
this.state = {
  info: { name: 'kobe', age: 30 }
}
render {
  const { info } = this.state
  <Home name={"coderwhy"} age={18} />
  // 可以使用这种方式一层一层传递到需要的组件
  <Home {...info} />
}

// Home
render() {
  const { name, age } = this.props
  return (
    <div>
      Home: {name}-{age}
    </div>
  )
}
  • 方式二
// 新建一个文件 theme-context.js
import React from "react"

const ThemeContext = React.createContext()

export default ThemeContext

// App
import ThemeContext from './context/theme-context'
<ThemeContext.Provider value={{ color: 'red', size: 30 }}>
  <Home {...info} />
</ThemeContext.Provider>

// Home
export class Home extends Component {
  render() {
    const { name, age } = this.props
    return (
      <div>
        Home: {name}-{age}
        <HomeInfo />
        <HomeBanner />
      </div>
    )
  }
}

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

function HomeBanner() {
  return (
    <ThemeContext.Consumer>
      {value => (
        <h2>
          Banner Info: {value.color}-{value.size}
        </h2>
      )}
    </ThemeContext.Consumer>
  )
}

export default HomeBanner
  • 方式三:非父子组件需要传递多个参数
// 此时需要新建多个Context文件
// user-context
import React from "react"

const UserContext = React.createContext()

export default UserContext

// App
import ThemeContext from './context/theme-context'
import UserContext from './context/user-context'
<UserContext.Provider value={{ nickname: 'kobe', age: 100 }}>
  <ThemeContext.Provider value={{ color: 'red', size: 30 }}>
    <Home {...info} />
  </ThemeContext.Provider>
</UserContext.Provider>

// Home
render() {
    const { name, age } = this.props
    return (
      <div>
        Home: {name}-{age}
        <HomeInfo />
        <HomeBanner />
      </div>
    )
  }
  
// HomeInfo
render() {
  return (
    <div>
      HomeInfo: {this.context.color}-{this.context.size}
      <UserContext.Consumer>
        {value => (
          <h2>
            HomeInfo2: {value.nickname}-{value.age}
          </h2>
        )}
      </UserContext.Consumer>
    </div>
  )
}

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

// event-bus.js
import { HYEventBus } from "hy-event-store"

const eventBus = new HYEventBus()

export default eventBus

// App
import React, { Component } from "react"
import Home from "./Home"
import eventBus from "./event-bus"

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

    this.state = {
      name: "",
      age: 0,
      height: 0
    }
  }
  componentDidMount() {
    eventBus.on("bannerPrev", this.prevClick, this)
    eventBus.on("bannerNext", this.nextClick, this)
  }
  prevClick(name, age, height) {
    console.log("监听到 bannerPrev")
    this.setState({
      name,
      age,
      height
    })
  }
  nextClick(info) {
    console.log("监听到 bannerNext", info)
  }
  componentWillUnmount() {
    eventBus.off("bannerPrev", this.prevClick)
    eventBus.off("bannerPrev", this.nextClick)
  }
  render() {
    const { name, age, height } = this.state
    return (
      <div>
        <h2>
          App Component-{name}-{age}-{height}
        </h2>
        <Home />
      </div>
    )
  }
}

export default App

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

export class Home extends Component {
  render() {
    return (
      <div>
        <h2>Home Component</h2>
        <HomeBanner />
      </div>
    )
  }
}

export default Home

// HomeBanner
import React, { Component } from "react"
import eventBus from "./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", rank: 99 })
  }
  render() {
    return (
      <div>
        <h2>HomeBanner Component</h2>
        <button onClick={e => this.prevClick()}>上一个</button>
        <button onClick={e => this.nextClick()}>下一个</button>
      </div>
    )
  }
}

export default HomeBanner

4. 组件的插槽实现

4.1 插槽实现的两种方式

4.1.1 方式一

// App
<Navbar>
  <div>left</div>
  <h2>title</h2>
  <i>right</i>
</Navbar>

// Navbar
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>
  )
}

// 可以使用 props 类型判断来控制传入的内容是元素还是数组
Navbar.propTypes = {
  // children: ProyTypes.element
  children: ProyTypes.array
}
  • 此方式实现的弊端:
    • 当props只有一个children时,此时插槽内容默认为一个元素,而不是数组。

4.1.2 方式二

// App
<NavBar leftSlot={<button>按钮</button>} centerSlot={'中心内容'} rightSlot={<i>斜体文字</i>} />

// NavBar
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>
  )
}

4.2 组件的作用域插槽

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

    this.state = {
      titles: ['流行', '精选', '新款'],
      tabIndex: 0
    }
  }
  indexChange(tabIndex) {
    this.setState({ tabIndex })
  }
  getTabItem(item) {
    if (item === '流行') {
      return <h2>{item}</h2>
    } 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.indexChange(i)} itemType={item => this.getTabItem(item)} />
        <h1>{titles[tabIndex]}</h1>
      </div>
    )
  }
}

// TabControl
export class TabControl extends Component {
  constructor() {
    super()

    this.state = {
      currentIndex: 0
    }
  }
  tabChange(i) {
    this.setState({ currentIndex: i })
    this.props.tabClick(i)
  }
  render() {
    const { titles, itemType } = this.props
    const { currentIndex } = this.state
    return (
      <div className="tab-control">
        {titles.map((title, index) => (
          <div className={`item ${currentIndex === index ? "active" : ""}`} key={title} onClick={e => this.tabChange(index)}>
            {/* <span className="text">{title}</span> */}
            // itemType 是一个函数,在此处是直接调用,并且将title参数传过去
            {itemType(title)}
          </div>
        ))}
      </div>
    )
  }
}

5. setState的原理解析

5.1 setState的三种写法

  • 基础方式调用
this.setState({
  message: "你好呀, 李银河"
})
  • setState 可以传入一个回调函数
// 好处一: 可以在回调函数中编写新的 state 逻辑
// 好处二: 当前的回调函数会将之前的 state 和 props 传递过来
this.setState((state, props) => {
  // 1. 可以编写一些对新的 state 处理逻辑
  // 2. 可以获取之前的 state 和 props 值
  console.log(this.state.message, this.props)

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

5.2 setState设计成异步

  • 在 react18之前,setTimeout 中 setState 操作,是同步操作
  • 在 react18之后,setTimeout 中 setState 操作,是异步操作(批处理)
  • 如果希望是同步操作,执行以下函数:
import { flushSync } from "react-dom"
flushSync(() => {
  this.setState({ message: "你好呀, 李银河" })
})
// 此时这个值就是上面修改后的值,而不是之前声明的message
console.log(this.state.message)

6. render函数性能优化

  • shouldComponentUpdate,简称 SCU
    • 本质上是通过判断shouldComponentUpdate的返回值是true还是false来决定页面是否更新,如果是true,则更新;反之,不更新。
// App
// newProps、newState 是此组件修改后的值,而 this.state, this.props 中的值是上一次的结果值
shouldComponentUpdate(newProps, newState) {
  // App 进行性能优化的点
  if (this.state.message !== newState.message || this.state.counter !== newState.counter) {
    return true
  }
  return false
}

// Home
shouldComponentUpdate(newProps, nextState) {
  // 自己对比 state 是否发生改变: this.state & nextState
  if (this.props.messgae !== newProps.messgae) {
    return true
  }
  return false
}
  • 这种方式的写法弊端很明显:当组件内容过多时,这种方式很繁琐,每个变量都需要一个一个进行判断。
  • 改进方式:
    • 类组件:
      • 继承自PureComponent
      • export class Recommend extends PureComponent {}
    • 类组件:
      • 使用 memo 包裹
      • const myFunc = memo(function () {})

7. 数据不可变的力量

  • 一般情况下,state 中的值是可以直接修改的,但是在继承 PureComponent 后,直接修改 state 中的值是没有变化的 。如果要修改,需要将 state 中的对象浅层拷贝一份,在新的对象中进行修改,之后在将修改好后的值赋值给state。
// 1. 直接修改原来的 state, 重新设置一遍
// 在 PureComponent 中是不能引起重新渲染(re-render)
this.state.books.push(newBook)
this.setState({books: this.state.books})
  • 正确的修改方式
// 2. 复制一份 books, 在新的 books 中修改, 设置新的 books
const books = [...this.state.books]
books.push(newBook)
this.setState({ books })

8. ref获取DOM元素和组件

8.1 ref获取DOM

  • 在 React 元素上绑定一个 ref 字符串
getNativeDOM() {
  // console.log(this.refs.lwz)
}

...
<h2 ref="lwz">Hello React</h2>
<button onClick={e => this.getNativeDOM()}>获取DOM元素</button>
  • 提前创建好ref对象, 将创建好的对象绑定到元素
getNativeDOM() {
  console.log(this.titleRef.current)
}
...
constructor() {
  super()

  this.titleRef = createRef()
}
<button onClick={e => this.getNativeDOM()}>获取DOM元素</button>
<h2 ref={this.titleRef}>Hello React</h2>
  • 传入一个回调函数, 在对应的元素被渲染之后,回调函数执行,并且将元素传入
constructor() {
  super()

  this.titleEl = null
}
getNativeDOM() {
  console.log(this.titleEl)
}

...
<h2 ref={el => (this.titleEl = el)}>Hello React</h2>
<button onClick={e => this.getNativeDOM()}>获取DOM元素</button>

8.2 ref获取类组件实例

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

export class HelloWorld extends PureComponent {
  constructor() {
    super()

    this.state = {}
  }
  test() {
    console.log("-----------")
  }
  render() {
    return <h2>Hello React</h2>
  }
}

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

    this.hwRef = createRef()
  }
  getComponent() {
    this.hwRef.current.test()
  }
  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef} />
        <button onClick={e => this.getComponent()}>获取类组件实例</button>
      </div>
    )
  }
}

export default App

8.3 ref获取函数式组件

  • 需要使用到forwardRef
import React, { createRef, forwardRef, PureComponent } from 'react'

const HelloWorld = forwardRef(function (props, ref) {
  return (
    <div>
      <h2 ref={ref}>Hello React</h2>
      <p>Hello World</p>
    </div>
  )
})

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

    this.hwRef = createRef()
  }
  getComponent() {
    console.log(this.hwRef.current)
  }
  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef} />
        <button onClick={e => this.getComponent()}>获取类组件实例</button>
      </div>
    )
  }
}

export default App

9. 受控组件和非受控组件

  • 两者的区别在于组件是否由 React 来控制。如果希望变成受控组件,那么需要将组件的值和 state 值关联起来,并且在组件的值变化的时候触发事件,通过此事件来修改 state 的值。
import React, { PureComponent } from "react"

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

    this.state = {
      username: "kobe"
    }
  }
  inputChange(event) {
    console.log("inputChange: ", event.target.value)
    this.setState({ username: event.target.value }, console.log(this.state.username))
  }
  render() {
    const { username } = this.state
    return (
      <div>
        {/* 受控组件 */}
        <input type="text" value={username} onChange={e => this.inputChange(e)} />

        {/* 非受控组件 */}
        <input type="text" onChange={e => this.inputChange(e)} />
        <h2>username: {username}</h2>
      </div>
    )
  }
}

export default App