React学习笔记 --- 受控组件和非受控组件及ref

1,356 阅读8分钟

一、 不可变数据的力量

示例代码

import React, { Component } from 'react';

class App extends Component {

  constructor(props) {
     super(props)

    this.state = {
      friends: [
        { name: 'Klaus' },
        { name: 'Steven' },
        { name: 'Ted' },
      ]
    }
  }

  render() {
    return (
      <div>
        <h2>好友列表</h2>
        <ul>
          {
            this.state.friends.map(item => <li key={ item.name }>{ item.name }</li>)
          }
        </ul>
        <button onClick={ () => this.insertItem() }>insert</button>
      </div>
    )
  }

  shouldComponentUpdate(nextProps, nextState) {
    return nextState.friends !== this.state.friends
  }

  insertItem() {
    this.state.friends.push({
      name: 'Kobe'
    })

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

export default App;

效果

wKwYYn.gif

发现 虽然修改了state中的状态,但是界面中的数据并没有发生任何的修改

这是因为shouldComponentUpdate函数返回的是false,

this.state.friends.push( ... )方法,会修改原始数组,并且返回新数组的长度

那么此时this.state === newState,所以此时界面是不会执行render函数的

所以,我们在开发中不应该修改state中的数据后再执行setState方法,这样界面是不会执行render函数的

参考 React文档-性能优化

所以上述的应进行如下的修改:

 insertItem() {
    this.setState({
      friends: [ ...this.state.friends, {
        name: 'Kobe'
      } ]
    })
  }

wKBZxP.png wKBmKf.png

根据如上的示意图可以数组的解构只是一层浅拷贝,其根据数组中每一项的引用生成了一个新的数组

而之后的数据插入是在新数组中进行操作的,所以在setState中的statenewState是值不同的2个对象,

所以会调用render方法,重新渲染界面

示例

import React, { PureComponent } from 'react';

class App extends PureComponent {

  constructor(props) {
     super(props)

    this.state = {
      friends: [
        { name: 'Klaus', age: 18 },
        { name: 'Steven', age: 23 },
        { name: 'Ted', age: 35 },
      ]
    }
  }

  render() {
    return (
      <div>
        <h2>好友列表</h2>
        <ul>
          {
            this.state.friends.map((item, index) => (
              <li key={ item.name }>
                { item.name } { item.age }
                <button onClick={ () => this.increment(index) }>increment</button>
              </li>
            ))
          }
        </ul>
      </div>
    )
  }

  increment(index) {
    this.state.friends[index].age += 1

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

export default App;

wKrj8s.gif

可以看到的是,this.state.friends[index].age += 1修改的依旧是state数据本身

所以没有调用render函数,界面依旧是没有发生任何的变化

因此进行如下修改:

  increment(index) {
    const newFriends =  [ ...this.state.friends ]
    newFriends[index].age += 1

    this.setState({
      friends: newFriends
    })
  }
}

newFriends 只是friends的浅拷贝,所以本质上修改newFriends中的age,就是修改friends中的age

但是因为newFriendsfriends的地址值是不用的,所以shallowEqual方法的返回值为false,所以依旧会调用render函数

二、 事件总线

前面通过Context主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?

  • 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus),来完成操作;
  • 在React中,我们可以依赖一个使用较多的库 events 来完成对应的操作;我们可以通过npm或者yarn来安装events
import React, { PureComponent } from 'react'

// 引入事件发射器(在写项目的时候,可以把全局的变量引入到一个单独的文件中)
import { EventEmitter } from 'events'

// 定义事件总线
// EventBus 本质上就是一个全局对象,
// 主要用于多个组件之间进行跨组件事件传递
const EventBus = new EventEmitter()

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

    this.state = {
      msg: '这是测试数据'
    }
  }

  // 在这里监听事件
  componentDidMount() {
    // 这里的事件是交给Event去回调的,不是我们自己去回调的,
    // 所以这里依旧需要包裹上箭头函数,否则内部this的指向会是emitEvent
   EventBus.addListener('handleEventEmit', (msg, num) => this.handleEventEmit(msg, num))
  }


  // 在这里移除事件, 需要和componentDidMount中的绑定的事件保持一一对应
  componentWillUnmount() {
    // 这样移除的是EventBus中所有的handleEventEmit
    // EventBus.removeListener('handleEventEmit')

    // 如果只希望移除当前组件中的handleEventEmit事件
    EventBus.removeListener('handleEventEmit', this.handleEventEmit)

  }

  // 这个事件必须定义在外面
  // 因为addListener中的事件对象,必须和 removeListener中的事件对象保持一致才可以被移除
  handleEventEmit(msg, num) {
    // console.log(msg, num) // Hello World 23
    this.setState({
      msg
    })
 }

  render() {
    return (
      <div>
        Header
        <span>
          { this.state.msg }
        </span>
      </div>
    )
  }
}

class Footer extends PureComponent {
  render() {
    return (
      <div>
        Footer
        <button onClick={ () => this.handleEmit() }>点我发射事件</button>
      </div>
    )
  }

  handleEmit() {
    // 发射事件
    EventBus.emit('handleEventEmit', 'Hello World', 23)
  }
}

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

三、ref的使用

在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作,此时就可以使用ref(reference)

react中获取原生DOM的方式:

  1. ComponentDidMount方法中使用原生的方式(document.getElementById)来获取DOM元素
  2. 使用ref属性

3.1 如何创建refs来获取对应的DOM

  1. 方式一:传入字符串
    • 使用时通过 this.refs.传入的字符串格式获取对应的元素;
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <h2 ref="titleRef">Hello World</h2>
        <button onClick={ () => this.changeText() }>changeText</button>
      </div>
    )
  }

  changeText() {
    // this.refs.changeText --- 就是原生的DOM对象
    this.refs.titleRef.innerHTML = 'Hello React'
  }
}
  1. 方式二:传入一个对象 官方推荐
    • 对象是通过 React.createRef() 方式创建出来的;
    • 使用时获取到创建的对象其中有一个current属性就是对应的元素;
// 这里需要多引入一个函数 createRef
import React, { PureComponent, createRef } from 'react'

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

    //  注意这个是单独写的,不是在state中
     this.titleRef = createRef()
  }

  render() {
    return (
      <div>
        {/* 这里传递一个特别的对象 */}
        <h2 ref={ this.titleRef }>Hello World</h2>
        <button onClick={ () => this.changeText() }>changeText</button>
      </div>
    )
  }

  changeText() {
    // 对象里面有一个current属性,存储的就是对应的原生DOM元素
    this.titleRef.current.innerHTML = 'Hello React'
  }
}
  1. 方式三:传入一个函数
    • 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;
    • 使用时,直接拿到之前保存的元素对象即可;
import React, { PureComponent } from 'react'

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

    // 这里定义需要使用的ref变量
    this.titleRef = null
  }

  render() {
    return (
      <div>
        {/* 在回调函数中的形参就是原生dom对象 */}
        <h2 ref={ args => this.titleRef = args }>Hello World</h2>
        <button onClick={ () => this.changeText() }>changeText</button>
      </div>
    )
  }

  changeText() {
    this.titleRef.innerHTML = 'Hello React'
  }
}

3.2 ref的类型

ref 的值根据节点的类型而有所不同:

  • 当 ref 属性用于 HTML 元素时,获取的就是原生DOM对象;
  • 当 ref 属性用于自定义 class 组件时,获取的就是自定义的class组件;
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例,所以也就无法使用ref;

示例 --- 在父组件中调用子组件的事件

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

class Cpn extends PureComponent {
 render() {
   return (
     <h2 onClick={ () => this.handleClick() }>Hello React</h2>
   )
 }

 handleClick() {
   console.log('子组件的函数被调用了')
 }
}

export default class App extends PureComponent {
  constructor(props) {
    super(props)
    this.cpnRef = createRef()
  }

  render() {
    return (
      <div>
        <Cpn ref={ this.cpnRef } />
        <button onClick={ () => this.clickHandler() }>点我</button>
      </div>
    )
  }

  clickHandler() {
    // this.cpnRef ---- 对应的Cpn子组件

    // 在父组件调用子组件中的方法
    this.cpnRef.current.handleClick()
    // result: 子组件的函数被调用了
  }
}

函数式组件是没有实例的,所以无法通过ref获取他们的实例:

  • 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
  • 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref

四、 受控组件

无论是受控组件还是非受控组件都是指的是表单元素

在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state

比如下面的HTML表单元素:

  • 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
  • 在React中,并没有禁止这个行为,它依然是有效的;
  • 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
  • 实现这种效果的标准方式是使用“受控组件”

示例

react中的数据流是单向数据流

输入 --- 修改 state ,state中发生改变 --- 修改界面中的展示

import React, { PureComponent } from 'react'

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

    this.state = {
      username: ''
    }
  }
  render() {
    return (
      {/*
      	使用react合成的表单提交事件
      	来阻止默认的表单提交行为
      	并监听对应的提交事件
      */}
      <form onSubmit={ e => { this.handleSubmit(e) } }>
        <label htmlFor="username">
          userName:
          {/*
          	使用onChange事件来修改state中的对应状态
          	使用value值来使用控件的值来源于state中的对应状态
          */}
          <input id="username" type="text" onChange={ e => this.handleChange(e) } value={ this.state.username } />
          <input type="submit" value="submit" />
        </label>
      </form>
    )
  }

  handleChange(e) {
    // e.target.value
    // input输入框输入的值
    // 因为react是合成事件对象,所以原生事件所有的属性 在合成事件对象的身上其依然存在且可以正常使用

    this.setState({
      username: e.target.value
    })
  }

  handleSubmit(e) {
    // 阻止默认事件
    e.preventDefault()

    // 输出username
    console.log(this.state.username)
  }
}

wwXX0P.gif

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

而在 React 中 可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

  • 我们将两者结合起来,使React的state成为“唯一数据源”;

    • 由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源
  • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;

  • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;

    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 React, { PureComponent } from 'react'

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

    this.state = {
      fruits: [
        'apple',
        'banana',
        'orange'
      ],
      fruit: 'orange'
    }
  }

  render() {
    return (
      <form onSubmit={e => this.handleSubmit(e)}>
        <select name="fruit"
                onChange= {e => this.handleChange(e)}
                value={this.state.fruit}
        >
          {
            this.state.fruits.map(item => (
              <option key={item}>
                { item }
              </option>
            ))
          }
        </select>
        <button>submit</button>
      </form>
    )
  }

  handleChange(e) {
    this.setState({
      fruit: e.target.value
    })
  }

  handleSubmit(e) {
    e.preventDefault();

    console.log(this.state.fruit)
  }
}

处理多输入情况

import React, { PureComponent } from 'react';

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

    this.state = {
      username: '',
      password: ''
    }
  }
  render() {
    return (
      <form onSubmit={ e => this.handleSubmit(e) }>
        <label htmlFor="username">
          <span>userName:</span>
          <input
            type="text"
            name="username"
            value={ this.state.username }
            onChange={e => this.handleChange(e)}
          />
        </label>
		
        {/*
			label是行内块元素,所以所有的label都会在一行,
			如果需要换行可以使用br标签,或者在label外包裹一层div标签
		*/}
        
        <label htmlFor="password">
          <span>password:</span>
          <input
            type="password"
            name="password"
            value={ this.state.password }
            onChange={e => this.handleChange(e)}
          />
        </label>
        <button>submit</button>
      </form>
    )
  }

  handleChange(e) {
    // 使用ES6中的计算属性名 (Computed property names)
    this.setState({
      [e.target.name]: e.target.value
    })
  }

  handleSubmit(e) {
    e.preventDefault()

    const {username, password} = this.state
    console.log(username, password)
  }
}

export default App;

五、 非受控组件

React推荐大多数情况下使用 受控组件 来处理表单数据:

  • 一个受控组件中,表单数据是由 React 组件来管理的;

  • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;

如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据。

  • 使用ref来获取input元素;
  • 在非受控组件中通常使用defaultValue来设置默认值;
  • 同样,<input type="checkbox"> <input type="radio"> 支持 defaultChecked, <select><textarea>支 持 defaultValue。
import React, { PureComponent,createRef } from 'react'

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

    this.usernameRef = createRef()
    this.passwordRef = createRef()
  }
  render() {
    return (
      <form onSubmit={ e => this.handleSubmit(e) }>
        username: <input type="text" ref={ this.usernameRef } defaultValue='klaus' />
        passeord: <input type="password" ref={ this.passwordRef } />
        <button>submit</button>
      </form>
    )
  }

  handleSubmit(e) {
    e.preventDefault()

    console.log(this.usernameRef.current.value, this.passwordRef.current.value)

  }
}

因为非受控组件是自己去操作原生DOM,来获取对应的元素值,所以这种方式在react中是不被推荐的

上一篇 React更新流程 下一篇 高阶组件和组件补充