React组件化

183 阅读15分钟

根据定义方式,可分为

  • 函数组件
  • 类组件

根据内部是否有状态需要维护,可分为

  • 无状态组件
  • 有状态组件

根据职责,可分为

  • 展示型组件
  • 容器型组件

类组件

  1. 定义一个(类名大写,组件名称必须是大写,小写会被认为是html元素),继承自React.Component;
  2. constructor可选,通常初始化一些数据;
  3. this.state中维护组件内部数据;
  4. class中必须实现render方法(render当中返回的jsx内容,就是之后React会帮助我们渲染的内容);

render函数的返回值

  • react元素(通过jsx写的代码,组件也算react元素)
  • 数组 (会遍历数组元素并显示)或 fragments
  • portals:可以渲染子节点到不同的DOM子树中
  • 字符串数值类型,在DOM中会被渲染为文本节点
  • 布尔类型null:什么都不渲染

数据

组件中的数据,可以分成2类:

  • 参与界面更新的数据:当数据变化时,需要更新组件渲染的内容
  • 不参与界面更新的数据:反之

参与界面更新的数据也可以称之为参与数据流,这些数据定义在当前对象的state中;

可以通过在构造函数this.state = {数据};

数据发生变化时,可以调用this.setState来更新数据,并且通知React进行update操作;

update操作时,就会重新调用render函数,并使用最新的数据,来渲染界面;

class App extends React.Component {
  // 组件数据
  constructor() {
    super()
    this.state = {
      msg: 'hello'
    }
  }
  // 组件方法
​
  // 渲染内容 render方法
  render() {
    return (
      <div>
        <h2>{this.state.msg}</h2>
        <button>修改文本</button>
      </div>
    )
  }
}
​
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)

函数式组件

返回值和类组件render函数返回值一样

特点(hooks出现之前)

  • 无生命周期,也会被更新并挂载,但是没有生命周期函数
  • this不能指向组件实例,因为没有组件实例;
  • 没有内部状态
function App() {
    return (
        <h2>123</h2>
    )
}

生命周期

创建到销毁的过程,叫生命周期;

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

生命周期函数

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

  • 比如实现componentDidMount函数,组件已经挂载到DOM上时,就会回调;
  • 比如实现componentDidUpdate函数,组件已经发生了更新时,就会回调;
  • 比如实现componentWillUnmount函数,组件即将被移除时,就会回调;

谈及React的生命周期时,主要是类的生命周期函数式组件没有生命周期,不过可以通过hooks来模拟一些生命周期函数的回调)

执行顺序

mount阶段:

  • 执行类的constructor方法;
  • 执行render方法;
  • React更新DOMRefs
  • 执行componentDidMount方法

update阶段:

  • 执行setState方法;
  • 执行render方法;
  • React更新DOMRefs
  • 执行componentDidUpdate方法;

unMount阶段:

  • 当组件被卸载,会执行componentWillUnmount方法

操作建议

constructor

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

通常只做两件事:

  • 初始化state;
  • 为事件绑定this;

componentDidMount

  • 依赖于DOM的操作
  • 发送网络请求(官方建议)
  • 添加一些订阅(会在componentWillUnmount取消订阅)

componentDidUpdate

  • 若对更新前后的props进行了比较,也可以在此处进行网络请求(例如当props未发生变化时,不发送网络请求)

componentWillUnmount

  • 清除、取消操作

不常用生命周期

shouldComponentUpdate

当该函数返回false时,则不会重新执行render函数,反之则会;

getSnapshotBeforeUpdate

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

组件通信

父传子

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

父组件

import React, { Component } from 'react'
import Header from './Header'
​
class Main extends Component {
  constructor() {
    super()
    this.state = {
      list: [1, 2, 3]
    }
  }
  render () {
    const { list } = this.state
    return (
      <Header list={list} />
    )
  }
}

子组件

import React, { Component } from 'react'
​
class Header extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }
​
  render () {
    const { list } = this.props
    return (
      <ul>
        {
          list.map((item) => {
            return (
              <li key={item}>{item}</li>
            )
          })
        }
      </ul>
    )
  }
}

当constructor接收的参数props传递给super时,内部将props保存在当前实例中,类似进行了 this.props = props

constructor也可以省略,内部默认进行保存props操作;

props类型限制

对于大型项目来说,传递的数据应该进行类型检查(防止”字符串调用map“这种错误)

  • Flow
  • TypeScript
  • prop-types库

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

import React, { Component } from 'react'
import PropsTypes from 'prop-types'class Header extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }
​
  render () {
    const { list } = this.props
    return (
      <ul>
        {
          list.map((item) => {
            return (
              <li key={item}>{item}</li>
            )
          })
        }
      </ul>
    )
  }
}
​
Header.propsTypes = {
  list: PropsTypes.array.isRequired
}

通过组件实例的propsTypes属性,设置了list是数组类型且是必传的props

若非必传,可以是设置默认值(可以避免undefined问题)

Header.propsTypes = {
  list: PropsTypes.array.isRequired
}
Header.defaultProps = {
    list: []
}

可以限制的类型有

  • array
  • bool
  • func
  • number
  • object
  • string
  • symbol
  • node
  • element

子传父

子组件如何向父组件传递消息?

  • 在vue中是通过自定义事件来完成;
  • 在react中同样通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这回调函数;

父组件main.jsx

import React, { Component } from 'react'
import Header from './Header'class Main 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>
        <Header addClick={(count) => this.changeCount(count)} />
      </div>
    )
  }
}

子组件header.jsx

import React, { Component } from 'react'
​
class Header extends Component {
  add(count) {
    this.props.addClick(count)
  }
  render () {
    const { add } = this
    return (
      <div>
        <button onClick={e => add(1)}>+1</button>
      </div>
    )
  }
}

当子组件的按钮点击之后,会调用父组件传过来的props中的addClick() ;

从而通知父组件去调用changeCount() ,去修改父组件的数据

案例

父组件App.jsx

import React from "react"
import TabControl from "./TabControl"
class App extends React.Component {
  constructor() {
    super()
    this.state = {
      titles: ['流行', '新品', '精选'],
      tabIndex: 0
    }
  }
  changeTab (index) {
    this.setState({
      tabIndex: index
    })
  }

  render () {
    const { titles, tabIndex } = this.state
    return (
      <div>
        <TabControl titles={titles} tabClick={(index) => this.changeTab(index)} />
        <h1>{titles[tabIndex]}</h1>
      </div>
    )
  }
}

子组件TabControl.jsx

import React, { Component } from 'react'
import "./style.css"

class TabControl extends Component {
  constructor(props) {
    super(props)

    this.state = {
      currentIndex: 0
    }
  }
  itemClick (index) {
    this.setState({
      currentIndex: index
    })
    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={() => this.itemClick(index)}
              >
                <span className='text'>{item}</span>
              </div>
            )
          })
        }
      </div>
    )
  }
}

style.css

.tab-control {
  display: flex;
  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提供了一个API:Context

Context提供了一种组件间共享某些数据的方案,比如当前认证得用户、主题或首选语言;

context的基本使用

React.createContext参数有个defaultValue,如果不是后代组件关系(兄弟组件),可以从defaultValue取到共享的数据

  1. 使用React.createContext创建出context(每个context对象都会返回一个Provider组件,它允许消费组件订阅context的变化);
  2. 通过context的Provider中的value属性为后代提供希望共享的数据;
  3. 后代设置contextType为指定context(可以多个context);
  4. 然后可以获取到那些数据了;

context.js

import React from "react"
const ThemeContext = React.createContext()
export default ThemeContext

App.jsx

import React from "react"
import Home from "./Home"
import ThemeContext from "./context"

class App extends React.Component {
  render () {
    return (
      <div>
        <h2>App</h2>
        <ThemeContext.Provider value={{ color: 'red', size: '30' }}>
          <Home></Home>
        </ThemeContext.Provider>
      </div>
    )
  }
}

Home.jsx

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

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

HomeInfo.jsx

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

class HomeInfo extends Component {
  render () {
    const { color } = this.context
    return (
      <div>
        <h2>HomeInfo:{color}</h2>
      </div>
    )
  }
}
HomeInfo.contextType = ThemeContext

函数式组件共享context

在类组件中可以使用this拿到context;

而函数式组件中this拿不到,怎么做呢?

context.Consumer也可以订阅到context的变更(当组件中需要使用多个context也可以使用Consumer);

需要一个函数作为子元素,通过该函数的参数value传递当前的context;

import ThemeContext from "./context"
function HomeBannar () {
  return (
    <div>
      <h2>HomeBannar</h2>
      <ThemeContext.Consumer>
        {
          (value) => {
            return <h2>{value.color}</h2>
          }
        }
      </ThemeContext.Consumer>
    </div>
  )
}

事件总线EventBus

context实现跨组件传递数据只能从根开始,要是需要兄弟组件之间传递呢?事件总线

先安装相关的库,比如event-bus

event-bus.js

import { HYEventBus } from 'hy-event-store'

const eventBus = new HYEventBus()

export default eventBus

然后发射事件

import eventBus from './event-bus'

...
preClick() {
  eventBus.emit('bannerPrev', 10)
}

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

在组件挂载完成后,可以监听事件

import eventBus from './event-bus'

...
componentDidMount() {
  eventBus.on('bannerPrev', (val) => {
    console.log(val)
  })
}

在组件销毁后,要移除事件监听;

方便在eventBus.off() 传递函数,在eventBus.on() 传递的函数应该抽离成单独的函数

import eventBus from "./event-bus"

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

bannerPrevClick(val) {
  console.log(val)
}

componentWillUnmount() {
  eventBus.off('bannerPrev', this.bannerPrevClick)
}

然而还有个问题:bannerPrevClick在运行时找不到this(当前组件实例),这样就无法调用setState()

可以将bannerPrevClick定义成箭头函数,或者显示绑定

import eventBus from "./event-bus"

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

bannerPrevClick = (val) => {
  console.log(val)
}

componentWillUnmount() {
  eventBus.off('bannerPrev', this.bannerPrevClick)
}

import eventBus from "./event-bus"

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

bannerPrevClick(val) {
  console.log(val)
}

componentWillUnmount () {
  eventBus.off('bannerPrev', this.bannerPrevClick)
}

实现插槽方案

react中有两种实现插槽的方式:

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

props的children属性

  • props中有一个children属性,是个数组,存放着多个子元素;
  • 若只有一个子元素,则children不是数组,而是该子元素本身(缺点);

父元素

class App extends Component {
  render () {
    return (
      <div>
        <NavBar>
          <button>按钮</button>
          <h2>标题</h2>
          <i>斜体文字</i>
        </NavBar>
      </div>
    )
  }
}

子元素

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

通过children子元素实现插槽效果,还有个缺点,就是需要索引精准匹配;

props传递React子元素

父元素

render () {
    return (
      <div>
        <NavBar
          leftSlot={<button>按钮</button>}
          centerSlot={<h2>标题</h2>}
          rightSlot={<i>斜体文字</i>}
        />
      </div>
    )
  }

子元素

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

作用域插槽

希望复用某个组件;

但是该组件展示数据的方式可能不符合预期

父组件希望能决定数据的每一项该以什么样的方式展示;

这时候就可以使用作用域插槽啦

如何取到这每一项呢? 通过函数

父组件

constructor() {
  super()
  this.state = {
    titles: ['流行', '新品', '精选'],
    tabIndex: 0
  }
}
changeTab (index) {
  this.setState({
    tabIndex: index
  })
}

render () {
  const { titles, tabIndex } = this.state
  return (
    <div>
      <TabControl
        titles={titles}
        tabClick={(index) => this.changeTab(index)}
        itemType={(item) => <button>{item}</button>}
      />
      <h1>{titles[tabIndex]}</h1>
    </div>
  )
}

子组件

constructor(props) {
super(props)

this.state = {
  currentIndex: 0
}

emClick (index) {
this.setState({
  currentIndex: index
})
this.props.tabClick(index)


nder () {
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={() => this.itemClick(index)}
          >
            {itemType(item)}
          </div>
        )
      })
    }
  </div>
)

父组件中,使用TabControl组件时增加一个props(itemType);

itemType的值是一个函数,这个函数决定每一项数据子组件中的展示方式;

而子组件通过props,可以调用这个函数,并且通过参数,可以传递每一项数据交给父组件

setState

为什么使用它

修改了state之后,希望React根据最新的state来重新渲染界面,但是React不知道数据发生了变化;

React并没有数据劫持,而Vue2使用Object.defineProperty或者Vue3使用Proxy来监听数据的变化;

需要通过setState来告知React,数据发生了变化;

用法

用法1:传入一个对象

setState({
    msg: 1
})

内部调用Object.assign(this.state, newState) ,这个对象会和state合并,将指定属性的值覆盖

用法2:传入一个回调函数

这个函数返回一个对象

setState(() => {
  return {
      msg: 1
  }
})

这种方式和传入一个对象类似,那这种方式有什么好处呢?

1)可以编写对新state的处理逻辑,内聚性更强

2)当前回调函数可以传递之前的state和props

用法3:传入第二参数(callback)

setState在React的事件处理中是一个异步调用,不会立即完成,也不会阻塞其它代码;

如果希望数据合并之后进行一些逻辑处理,就可以在第二个参数传入一个回调函数;

this.state = {
    msg: 0,
    name: 'hhh'
}

...
setState({ msg: 1 }, () => {
	console.log(this.state.msg)// 1
})
console.log(this.state.msg)// 0

为什么设计成异步

1)可以显著提升性能

  • 若每次调用setState都进行一次更新,意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
  • 最好的办法应该是获取到多个更新,之后进行批量更新,只执行一次render函数;

2)若同步更新了state,但还没有执行render函数,那state和props不能保持同步

而React18之前,有些情况setState是同步的

  • setTimeout
  • 原生dom事件

如果想把setState变成同步,立即拿到最新state,可以使用flushSync() ,这个函数在react-dom中;

flushSync(() => {
    this.setState({
        msg: '123'
    })
})
console.log(this.state.msg)

React性能优化

React在stateprops发生改变时,会调用React的render方法,创建出一棵新的树;

如果一棵树参考另外一棵树进行完全比较更新,那时间复杂度将是O(n²)

这开销会有点大,于是React进行了优化,将其优化成了O(n) :

  • 只会同层节点比较,不会跨节点比较;
  • 不同类型的节点,产生不同的树结构;
  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;

shouldComponentUpdate

当一个组件的render函数被执行,那这个组件的那些子组件的render函数也会被执行;

如果那些子组件的state或者props并没有发生改变,那重新执行render函数是多余的、浪费性能的;

它们调用render函数应该有个前提:依赖的数据(state、props)发生改变时,再调用自己的render函数

如何控制render函数是否被调用:通过一个生命周期shouldComponentUpdate方法,很多时候简称SCU

该方法有两个参数:

  • 1)nextProps,修改之后的props
  • 2)nextState,修改之后的state

例如,在一个组件中

shouldComponentUpdate(nextProps, nextState) {
    if (this.state.msg !== nextState.msg) return true
    return false
}

只有msg发生改变才会重新执行它的render函数

可是,如果每个组件都要这样判断,那未免也太麻烦了

这时候React给我我们提供了PureComponent

PureComponent

若当前组件是类组件,可以继承PureComponent

对于props和state的判断,内部已经帮我做了,所以render函数就会根据需要来重新执行了;

不过,内部的比较是浅层的,使用shallowEqual()

后续开发类组件基本都是继承PureComponent

import { PureComponent } from 'react'

class App extends PureComponent {
  ...
  render () {
    return (
      <div>
        <h2>App</h2>
      </div>
    )
  }
}

memo

类组件才有生命周期shouldComponentUpdate,那函数式组件如何判断props是否发生改变呢?

使用react中的memo

import { memo } from 'react'

const Home = memo(function(props) {
  return <h2>home: {props.msg}</h2>
})

export default Home

数据不可变的力量

看个例子

import React from "react"

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

    this.state = {
      books: [
        { name: '你不知道的js', price: 99, count: 1 },
        { name: 'js高级程序设计', price: 88, count: 1 },
        { name: 'React高级程序设计', price: 78, count: 2 },
      ]
    }
  }

  addBooks () {
    const newBooks = { name: 'Vue高级程序设计', price: 66, count: 2 }
    this.state.books.push(newBooks)
    this.setState({ books: this.state.books })
  }

  render () {
    const { books } = this.state
    return (
      <div>
        <h2>数据列表</h2>
        <ul>
          {
            books.map((item, index) => {
              return (
                <li key={index}>
                  <span>name:{item.name}-price:{item.price}-counter:{item.count}</span>
                  <button>+1</button>
                </li>
              )
            })
          }
        </ul>
        <button onClick={e => this.addBooks()}>添加书籍</button>
      </div>
    )
  }
}

单独看addBooks

addBooks() {
  const newBooks = { name: 'Vue高级程序设计', price: 66, count: 2 }
  this.state.books.push(newBooks)
  this.setState({ books: this.state.books })
}

这里修改state中的books方法是直接修改,虽然也能成功,但是React不推荐!为什么呢?

如果将这类组件继承PureComponent而不是Component,那这种方法修改不成功;

而继承PureComponent的类组件内部会判断修改前后的state是否发生变化,并且是浅层的比较,从而决定是否重新执行render函数;

而这浅层的比较只是比较到books这一层(内存地址)是否变化,并没有比较books里面的内容

这种浅层比较,导致内部判断state没有发生变化(实际books内容已经变了),而不会重新执行render函数;

应该写成这样(组件先改成继承PureComponent)

addBooks () {
  const newBooks = { name: 'Vue高级程序设计', price: 66, count: 2 }
  const books = [...this.state.books]
  books.push(newBooks)
  this.setState({ books: books })
}

重新创建的books和state中books的内存地址不一样,内部判断state发生了变化,所以会重新执行render函数;

ref

获取原生dom

  • 在React元素上绑定一个ref字符串
  • 提前创建ref对象(通过current取到),createRef(),将创建出来的对象绑定到React元素(推荐
  • 传入一个回调函数,在对应的元素被渲染之后,回调函数被执行,并将该元素传入该回调函数
import React, { createRef, PureComponent } from 'react'

export class App extends PureComponent {
  constructor() {
    super()
    this.titleRef = createRef()
    this.titleEl = null
  }
  getDOM () {
    // 1.在React元素上绑定一个ref字符串
    console.log(this.refs.zsf) // 已废弃
    // 2.提前创建ref对象(通过current取到),createRef(),将创建出来的对象绑定到React元素
    console.log(this.titleRef.current)
    // 3.传入一个回调函数,在对应的元素被渲染之后,回调函数被执行,并将该元素传入该回调函数
    console.log(this.titleEl)
  }
  render () {
    return (
      <div>
        <h2 ref='zsf'>App1</h2>
        <h2 ref={this.titleRef}>App2</h2>
        <h2 ref={(el) => this.titleEl = el}>App2</h2>
        <button onClick={e => this.getDOM()}>获取DOM</button>
      </div>
    )
  }
}

获取组件实例

对于类组件,和获取原生dom类似

constructor() {
  super()
  this.hRef = createRef()
}
getDOM () {
  console.log(this.hRef.current)
}
render () {
  return (
    <div>
      <Hello ref={this.hRef} />
      <button onClick={e => this.getDOM()}>获取DOM</button>
    </div>
  )
}

函数式组件没有实例,但是在开发中可能想要获取函数式组件中某个元素的DOM,如何操作?

直接拿不到,但可以通过react提供的一个高阶函数forwordRef,接收一个函数(也就是传入函数式组件);

函数式组件第二个参数是接收一个ref

在App中创建的ref传给函数式组件Hello,而经过forwordRef转发,可以将其绑定函数式组件某个元素的DOM;

const Hello = forwardRef(function (props, ref) {
  return (
    <div>
      <h1 ref={ref}>hello</h1>
      <p>hhh</p>
    </div>
  )
})

class App extends PureComponent {
  constructor() {
    super()
    this.hRef = createRef()
  }
  getDOM () {
    console.log(this.titleRef.current)
  }
  render () {
    return (
      <div>
        <Hello ref={this.hRef} />
        <button onClick={e => this.getDOM()}>获取DOM</button>
      </div>
    )
  }
}

受控和非受控组件

受控组件

在HTML中,表单元素通常会自己维护state,并根据用户输入进行更新;

表单元素一旦绑定value值来自state中的属性,那么它就变成了受控组件

而React中没有双向绑定,是通过受控组件来控制input、textarea等表单元素;

想修改value需要监听表单元素的onChange事件,通过event.target.value获取最新的value值;

constructor() {
  super()
  this.state = {
    username: ''
  }
}

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

form表单

对于传统的form表单,默认是是会向服务器发起网络请求刷新页面

在React中需要监听onSubmit事件,使用event对象的preventDefault() 阻止这一默认行为;

constructor() {
  super()
  this.state = {
    username: ''
  }
}
handleSubmit (event) {
  event.preventDefault()
  console.log(this.state.username)
}
inputChange (event) {
  this.setState({ username: event.target.value })
}
render () {
  const { username } = this.state
  return (
    <div>
      <form onSubmit={e => this.handleSubmit(e)}>
        <label htmlFor="username">
          用户:<input id='username' type="text" value={username} onChange={e => this.inputChange(e)} />
        </label>
        <button type='submit'>提交</button>
      </form>
    </div>
  )
}

多个受控组件同个函数处理

constructor() {
  super()
  this.state = {
    username: '',
    password: ''
  }
}
handleSubmit (event) {
  event.preventDefault()
  console.log(this.state)
}
inputChange (event) {
  this.setState({ [event.target.name]: event.target.value })
}
render () {
  const { username, password } = this.state
  return (
    <div>
      <form onSubmit={e => this.handleSubmit(e)}>
        <label htmlFor="username">
          用户:<input id='username' name='username' type="text" value={username} onChange={e => this.inputChange(e)} />
        </label>

        <label htmlFor="username">
          密码:<input id='password' name='password' type="password" value={password} onChange={e => this.inputChange(e)} />
        </label>

        <button type='submit'>提交</button>
      </form>
    </div>
  )
}

处理多选表单

checkboxradio这两种表单用的是checked来保存状态,不是value;

constructor() {
  super()
  this.state = {
    hobbies: [
      { value: 'sing', text: '唱', isChecked: false },
      { value: 'dance', text: '跳', isChecked: false },
      { value: 'rap', text: 'rap', isChecked: false },
    ]
  }
}
handleSubmit (event) {
  event.preventDefault()

  console.log(this.state.hobbies)
}
inputChange (event, index) {
  const hobbies = [...this.state.hobbies]
  hobbies[index].isChecked = event.target.checked
  this.setState({ hobbies: hobbies })
}

render () {
  const { hobbies } = this.state
  return (
    <div>
      <form onSubmit={e => this.handleSubmit(e)}>
        爱好
        {
          hobbies.map((item, index) => {
            return (
              <label htmlFor={item.value} key={item.value}>
                <input id={item.value} type="checkbox" checked={item.isChecked} onChange={e => this.inputChange(e, index)} />
                <span>{item.text}</span>
              </label>

            )
          })
        }

        <button type='submit'>提交</button>
      </form>
    </div>
  )
}

select多选

select单选和普通表单写法类似,要是多选呢?

多选时,select元素的value得是数组

select如何获取用户选中的多个状态呢?event.target.selectedOptions

不过event.target.selectedOptions是类数组,不能使用一些数组的高阶函数;

可以使用Array.from() ,将它转化成数组;

constructor() {
  super()
  this.state = {
    fruit: ['orange']
  }
}
handleSubmit (event) {
  event.preventDefault()

  console.log(this.state.fruit)
}
inputChange (event) {
  const options = Array.from(event.target.selectedOptions)
  const values = options.map(item => item.value)
  this.setState({ fruit: values })
}

render () {
  const { fruit } = this.state
  return (
    <div>
      <form onSubmit={e => this.handleSubmit(e)}>
        水果
        <select value={fruit} onChange={e => this.inputChange(e)} multiple>
          <option value="apple">苹果</option>
          <option value="orange">橘子</option>
          <option value="banana">香蕉</option>
        </select>
        <button type='submit'>提交</button>
      </form>
    </div>
  )
}

非受控组件

而表单元素的value值交给浏览器维护,借助ref来获取

高阶组件

Higher-Order Components,简称HOC

高阶组件是参数为组件,返回值为新组件的函数;

可以对传入的组件拦截,然后可以进行props增强、登陆鉴权等等操作

应用场景:

  • props增强
  • 登陆鉴权
  • 劫持生命周期(比如计算渲染花费时间)

比如memo()forwardRef() 都是高阶组件

应用

props增强

不修改原有代码的情况下,添加新的props;

Home

const Home = enhancedUserInfo(function (props) {
  return <h1>Home: {props.name}- {props.level}</h1>
})

enhancedUserInfo

function enhancedUserInfo (Cpn) {
  class NewComponent extends PureComponent {
    constructor() {
      super()

      this.state = {
        userInfo: {
          name: 'zsf',
          level: 99
        }
      }
    }
    render () {
      return <Cpn {...this.state.userInfo} />
    }
  }
  return NewComponent
}

App

export class App extends PureComponent {
  render () {
    return (
      <div>
        <Home />
      </div>
    )
  }
}

实际应用场景---context

App

<div>
   <ThemeContext.Provider value={{ color: 'red', size: 30 }}>
     <Home />
   </ThemeContext.Provider>
 </div >

ThemeContext

import { createContext } from "react"
const ThemeContext = createContext()
export default ThemeContext

Home

class Home extends PureComponent {
  render () {
    const { color, size } = this.props
    return (
      <div>Home:{color} - {size}</div>
    )
  }
}

export default withTheme(Home)

withTheme

function withTheme(Cpn) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <Cpn {...value} {...props} />
          }
        }
      </ThemeContext.Consumer>
    )
  }
}

登陆鉴权

开发中,若需要判断用户是否登陆才能显示某个组件;

如果每个组件都需要自己判断,那太繁琐了;

可以编写个高阶组件:对每个组件进行鉴权判断,然后再决定是否显示;

App

render () {
  return (
    <div>
      <Home />
    </div >
  )
}

Home

export class Home extends PureComponent {
  render () {
    return (
      <div>home</div>
    )
  }
}

export default loginAuth(Home)

loginAuth

function loginAuth(Cpn) {
  return props => {
    const token = localStorage.getItem('token')

    if(token) {
      return <Cpn {...props}/>
    } else {
      return <h2>请先登陆</h2>
    }
  }
}

生命周期的劫持

应用类似。。。

意义

早期React提供组件之间复用代码的方式是mixin,目前已经不在建议使用;

mixin可能会相互依赖相互耦合,不利于代码维护;

HOC也是一种组件间复用代码的方式;

缺点

  • HOC需要在原组件上进行包裹或嵌套,若大量使用HOC,将会产生非常多的嵌套,这让调试变得困难;
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;

hooks的出现,是开创性的,它解决了很多React之前存在的问题,比如this指向hoc的嵌套复杂等等

portals

某些情况下,希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM的);

使用来自react-dom中的createPortal(内容,DOM元素)

fragment

如果不希望多渲染出一个根元素(经常使用div),可以使用fragment;

类似vue中的template

render () {
  return (
    <Fragment>
      <h1>1</h1>
      <span>2</span>
    </Fragment >
  )
}

fragment语法糖

render () {
  return (
    <>
      <h1>1</h1>
      <span>2</span>
    </>
  )
}

如果有key属性这不能省略fragment

StrictMode

StrictMode是一个用来突出显示应用程序中潜在问题的工具:

  • 与fragment一样,StrictMode不会渲染任何可见的UI;
  • 它为其后代元素触发额外的检查和警告;
  • 仅在开发模式下运行,不影响生产构建;
  • 可以为应用程序任何部分开启严格模式;

检测内容

  • 不安全的生命周期
  • 过时的ref API
  • 过时的context API
  • 意外的副作用(严格模式下会执行2次生命周期,看看是否有副作用)