React学习 --- 组件知识点补充

312 阅读7分钟

不可变数据

为什么setState中的数据需要是不可变的

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

不使用不可变数据

import { Component } from 'react'

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

    this.state = {
      users: [
        { name: 'Klaus' },
        { name: 'Steven' },
        { name: 'Alex' }
      ]
    }
  }

  render() {
    return (
      <>
        <ul>
          {
            this.state.users.map(user => <li key={user.name}>{ user.name }</li>)
          }
        </ul>
        <button onClick={ () => this.append() }>append user</button>
      </>
    )
  }

  append() {
    const userInfo = { name: 'Jhon' }
    this.state.users.push(userInfo)

    this.setState({
      users: this.state.users
    })
  }
}

此时功能看上去,没有什么大的问题,但是在实际开发中,我们一般会将组件继承自pureComponent或者自主去实现SCU

puerComponent默认实现的SCU方法本质上是 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)

而使用push方法更新数组,实际上是向原数组中插入数据,也就是oldState.users === newState.users

此时SCU方法就会返回false,并不会触发对应的render方法

因此,在React中使用setState进行数据更新的时候,一定要保证不去主动修改state中的数据,也就是保证state中的数据不可变性

import { PureComponent } from 'react'

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

    this.state = {
      users: [
        { name: 'Klaus' },
        { name: 'Steven' },
        { name: 'Alex' }
      ]
    }
  }

  render() {
    return (
      <>
        <ul>
          {
            this.state.users.map(user => <li key={user.name}>{ user.name }</li>)
          }
        </ul>
        <button onClick={ () => this.append() }>append user</button>
      </>
    )
  }

  append() {
    const userInfo = { name: 'Jhon' }
    const users = [...this.state.users, userInfo]

    this.setState({
      users
    })
  }
}

事件总线

Context主要实现的是数据的共享, 而且主要是爷孙级组件进行数据的共享

在开发中如果有跨组件之间的事件传递,可以使用eventsmitt这类第三方库来帮助我们进行实现

# 安装 -- 这里以events为例
$ yarn add events

events常用的API:

  • 创建EventEmitter对象: eventBus对象

  • 发出事件: eventBus.emit("事件名称", 参数列表)

  • 监听事件: eventBus.addListener("事件名称", 监听函数)

  • 移除事件: eventBus.removeListener("事件名称", 监听函数)

    • 如果removeListener值只传递了第一个参数,那么表示移除所有事件名为参数1的事件

    • 如果需要移除某一个事件所对应的具体事件处理函数,addListener中的函数和removeListener中的函数

      必须是同一个事件处理函数,也就是他们的引用地址必须是一致的

示例

import { PureComponent } from 'react'
import { EventEmitter } from 'events'

// 实际开发中,这个对象一般是一个全局对象,会被放置到工具js中在被导出
// 这里只是个小案例,所以直接在这里生成EventEmitter的实例对象
const events = new EventEmitter()

class Cpn extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      msg: 'default value'
    }
  }

  render() {
    return <h2>{ this.state.msg }</h2>
  }

  // 在componentDidMount进行事件订阅(监听)
  componentDidMount() {
    events.addListener('sendMsg', this.hanldSendMsg)
  }

  // 在componentWillUnmount取消事件订阅(监听)
  componentWillUnmount() {
    events.rawListeners('sendMsg', this.hanldSendMsg)
  }


  hanldSendMsg = (msg1, msg2) => {
    // 注意这里需要使用箭头函数
    // 因为事件被抽取后,传入EventEmitter对象进行使用的时候
    // 内部的this会被修改为EventEmitter对象,而非当前组件实例对象
    this.setState({
      msg: `${msg1} ${msg2}`
    })
  }
}

class Foo extends PureComponent {
  render() {
    return <button onClick={ () => this.emitEvent() }>click me</button>
  }

  emitEvent() {
    events.emit('sendMsg', 'Hello', 'World')
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Cpn />
        <Foo />
      </div>
    )
  }
}

ref

在React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素

但是某些特殊的情况,确实需要获取到DOM进行某些操作

获取DOM元素的方式有2种

  • componentDidMount事件中去使用document.getElementById之类的原生DOM方法去获取和操作DOM元素
  • 使用ref来获取和操作DOM元素

ref基本使用

  1. 字符串
    • 通过 this.refs.传入的字符串格式获取对应的元素
    • 早期react获取DOM元素的方式,现在已经不推荐使用
import { PureComponent } from 'react'

export default class App extends PureComponent {
  render() {
    return (<h2 ref="titleRef">Hello World</h2>)
  }

  componentDidMount() {
    console.log(this.refs.titleRef)
  }
}
  1. 对象
  • 对象是通过 React.createRef() 方式创建出来的
  • 使用时获取到创建的对象其中有一个current属性就是对应的元素
  • 目前react推荐的获取DOM的方式
import { PureComponent, createRef } from 'react'

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

    // 创建ref对象
    this.titleRef = createRef()
  }
  render() {
    return (<h2 ref={this.titleRef}>Hello World</h2>)
  }

  componentDidMount() {
    // 获取到的实际上一个对象 =>{ current: dom元素 }
    console.log(this.titleRef.current)
  }
}
  1. 函数
  • 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象
  • 使用时,直接拿到之前保存的元素对象即可
import { PureComponent } from 'react'

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

    this.titleRef = null
  }
  render() {
    return (<h2 ref={ el => this.titleRef = el }>Hello World</h2>)
  }

  componentDidMount() {
    console.log(this.titleRef)
  }
}

ref类型

  • 如果是普通DOM元素,那么使用ref获取到的就是普通的DOM元素
  • 如果是组件
    • 如果是类组件,那么获取到的就是类组件的实例对象
      • 注意是实例对象,不是DOM元素 [ 这里没有类似于vue中的el属性 ]
    • 如果是函数组件,那么无法使用ref获取到DOM元素对象,因为函数组件没有实例对象

受控组件

受控组件存在于表单中(其实在vue中对于这种书写方式提供了语法糖写法,就是v-model

如果一个表单元素满足以下两个条件,就可以认为这个表单元素是一个受控组件

  • 可变状态(mutable state)通常保存在组件的 state 属性中
  • 只能通过使用 setState()来更新
// 单值表单组件
import { PureComponent } from 'react'

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

    this.state = {
      username: ''
    }
  }

  render() {
    return (
      <form onSubmit={e => this.handleSubmit(e)}>
        {/* 此时这个input就是受控组件 */}
        用户名: <input
          type="text"
          onChange={e => this.handleUsernameChange(e)}
          value={ this.state.username }
        />
        <button>sumbit</button>
      </form>
    )
  }

  handleSubmit(e) {
    // 阻止表单的默认提交行为
    e.preventDefault()

    console.log(this.state.username)
  }

  handleUsernameChange(e) {
    this.setState({
      username: e.target.value
    })
  }
}
// 多值表单组件
import { PureComponent } from 'react'

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

    this.state = {
      users: [
        { name: 'Alex', checked: false },
        { name: 'Steven', checked: false },
        { name: 'Klaus', checked: false }
      ]
    }
  }

  render() {
    return (
      <form onSubmit={ e => this.handleSubmit(e) }>
        <ul>
          {
            this.state.users.map((user, index) => (
              <li key={user.name}>
                <input
                  type="checkbox"
                  name="users"
                  checked={user.checked}
                  onChange={ e => this.change(e, index) }
                />
                { user.name }
              </li>
            ))
          }
        </ul>
        <button>submit</button>
      </form>
    )
  }

  handleSubmit(e) {
    e.preventDefault()

    console.log(this.state.users)
  }

  change(e, i) {
    const users = [...this.state.users]
    users[i].checked = e.target.checked
    this.setState({
      users
    })
  }
}
ElementValue PropertyChange CallbackNew Value in the callback
<input type="text" />value="string"onChangeevent.target.value
<input type="checkbox" />checked = {boolean}onChangeevent.target.checked
<input type="radio" />checked = {boolean}onChangeevent.target.checked
<textarea />value = "string"onChangeevent.target.value
<select />value = "option value"onChangeevent.target.value
import { PureComponent } from 'react'

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

    this.state = {
      username: '',
      password: ''
    }
  }

  render() {
    return (
      <form onSubmit={e => this.handleSubmit(e)}>
        用户名: <input
          type="text"
          name="username"
          onChange={e => this.handleChange(e)}
          value={ this.state.username }
        />
        密码: <input
          type="text"
          name="password"
          onChange={e => this.handleChange(e)}
          value={ this.state.password }
        />
        <button>sumbit</button>
      </form>
    )
  }

  handleSubmit(e) {
    // 阻止表单的默认提交行为
    e.preventDefault()

    console.log(this.state.username, this.state.password)
  }

  handleChange(e) {
    // 当在受控组件上设置name属性的时候
    // 传入的合成事件对象上就存在一个name属性,其值就是我们在受控组件上设置的name属性
    this.setState({
      [e.target.name]: e.target.value
    })
  }
}

非受控组件

React推荐大多数情况下使用 受控组件 来处理表单数据,因为在一个受控组件中,表单数据是由 React 组件来管理的

但是如果某些情况下,我们就需要使用DOM操作来进行表单元素值的获取,而这类组件就被称之为非受控组件

原则上是不推荐我们在React中自己来操作DOM元素的,所以原则上也不推荐使用非受控组件

import { PureComponent, createRef } from 'react'

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

    this.usernameRef = createRef()
  }

  render() {
    return (
      <form onSubmit={e => this.handleSubmit(e)}>
        用户名: <input
          type="text"
          ref={this.usernameRef}
        />
        <button>sumbit</button>
      </form>
    )
  }

  handleSubmit(e) {
    e.preventDefault()

    // 在原生表单元素上有一个value属性
    // 可以帮助我们获取到表单元素的值
    console.log(this.usernameRef.current.value)
  }
}

非受控组件中通常使用defaultValue来设置默认值

同样,<input type="checkbox"> <input type="radio"> 支持 defaultChecked, <select><textarea> 支 持 defaultValue

render() {
  return (
    <form onSubmit={e => this.handleSubmit(e)}>
      {/*
          使用defaultValue设置元素的默认值
          而不是去使用原本的value属性
        */}
      用户名: <input
             type="text"
             defaultValue="Klaus"
             ref={this.usernameRef}
             />
      <button>sumbit</button>
    </form>
  )
}

Protals

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点

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

比如界面中的Model弹框

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案

  • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment
  • 第二个参数(container)是一个 DOM 元素

Cpn

import ReactDOM from 'react-dom'

export default function Cpn(props) {
  // 手动定义挂载点
  const div = document.createElement('div')
  div.setAttribute('id', 'model')
  document.body.appendChild(div)

  return ReactDOM.createPortal(props.children, document.getElementById('model'))
}

App

import { PureComponent } from 'react'
import Cpn from './Cpn'

export default class App extends PureComponent {
  render() {
    return <Cpn>
      <h2>Cpn1</h2>
      <h2>Cpn2</h2>
      <h2>Cpn3</h2>
    </Cpn>
  }
}

fragments

在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素

但很多情况下,内容外层的那个div其实是多余的,没有必要的

此时我们可以使用React内置的Fragment组件

React在编译的时候,会自动识别Fragment组件,其在使用上和div是一致的,只不过Fragment组件最终不会进行任何的渲染

import { PureComponent } from 'react'
export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      name: 'Klaus',
      age: 23
    }
  }

  render() {
    return (
      <div>
        {/*
          不使用fragment --- 有且必须要有一个根元素
        */}
        <span>name: { this.state.name }</span>
        <span>age: { this.state.age }</span>
      </div>
    )
  }
}

IjOHFh.png

import { PureComponent, Fragment } from 'react'
export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      name: 'Klaus',
      age: 23
    }
  }

  render() {
    return (
      <Fragment key="foo">
        {/*
          1. 在实际渲染的是Fragment并不会被渲染,其在功能上和div其实是一致的 
          2. 使用Fragment标签的时候,需要手动引入Fragment标签
          3. 可以在Fragment标签上添加对应的key
        */}
        <span>name: { this.state.name }</span>
        <span>age: { this.state.age }</span>
      </Fragment>
    )
  }
}

IjOJyS.png

import { PureComponent } from 'react'
export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      name: 'Klaus',
      age: 23
    }
  }

  render() {
    return (
      <>
        {/*
           1. <></>  ---- <Fragment></Fragment>的语法糖表示 --- 短语法表示
           2. 使用<></>的时候,不需要手动引入Fragment
           3. 不可以在<></>上添加任何的属性
         */}
        <span>name: { this.state.name }</span>
        <span>age: { this.state.age }</span>
      </>
    )
  }
}

StrictMode

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

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

严格模式的检查

  • 检测过时的API
    • 识别不安全的生命周期
    • 使用过时的ref API
  • 检查意外的副作用
    • 这个组件的constructor会被调用两次
    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作
    • 在生产环境中,是不会被调用两次的