React学习笔记 --- setState的简单使用

718 阅读6分钟

一、 setState的作用

不使用setState

import React, { Component } from 'react'

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

    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <div>
        <div>counter: { this.state.count }</div>
        <button onClick={ () => this.increment() }> +1</button>
      </div>
    )
  }

  increment() {
    this.state.count += 1
  }
}

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

  • 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变 化;
  • React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
  • 我们必须通过setState来告知React数据已经发生了变化

使用setState函数后

import React, { Component } from 'react'

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

    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <div>
        <div>counter: { this.state.count }</div>
        <button onClick={ () => this.increment() }> +1</button>
      </div>
    )
  }

  increment() {
    this.setState({
      count: this.state.count + 1
    })
  }
}

上述代码表明,在点击increment按钮后,state中的count自动递增1,且界面也实时发生了刷新

查看源码发现 setState方法是从Component继承过来的,其是定义在Component的原型上的

Component.prototype.setState = function(partialState, callback) {
 invariant(
     typeof partialState === 'object' ||
     typeof partialState === 'function' ||
     partialState == null,
     'setState(...): takes an object of state variables to update or a ' +
     'function which returns an object of state variables.',
 );
 this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

可见setState可以传递2个参数,一个为新的state对象,另一个为callback

二、setState的异步更新

示例

import React, { Component } from 'react'

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

    this.state = {
      message: 'Hello World'
    }
  }

  render() {
    return (
      <div>
        <div>{ this.state.message }</div>
        <button onClick={ () => this.changeText() }>change Text</button>
      </div>
    )
  }

  changeText() {
    this.setState({
      message: 'Hello React'
    })

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

wAtHpR.gif

在上述结果中可以看到,在虽然界面上的数据发生了改变,变为了Hello React

但是在控制台中的数据依旧是Hello World,由此证明了在React中setState是异步更新的

2.1 为什么setState是需要异步更新数据

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

  1. 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的; 最好的办法应该是获取到多个更新,之后进行批量更新
  • 在一次界面刷新中可能存在多个setState

  • 多个更新会统一放入队列中,合并在一起,进行批量操作

  1. 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步; state和props不能保持一致性,会在开发中产生很多的问题;

    • 因为setState的执行速度一般是大于render函数的执行速度的
    • 在执行render函数的过程中,render函数需要将JSX转换为ReactElement对象,并且需要通过diff算法来比较不同后再进行界面的刷新,这就导致了render函数的执行速度一般是比setState要慢的
    • 如果setState是同步更新的,那么就会导致在某一些时间点上,state中的数据发生了更新,但是render函数没有执行完毕,其中的state也就没有及时更新,如果此时没有更新的这个state数据又正好是另一个组件的props,那么可能导致在某一些时间点上,子组件中的props和父组件中对应的state中的状态是不一致的,这是十分危险的
    import React, { Component } from 'react'
    
    function Child(props) {
      return (
      <h2>{ props.name }</h2>
      )
    }
    
    export default class componentName extends Component {
      constructor(props) {
        super(props)
    
        this.state = {
          message: 'Hello World',
          name: 'Klaus'
        }
      }
    
      render() {
        return (
          <div>
            <div>{ this.state.message }</div>
            <button id='btn'>change Text</button>
    
            {/* 
                如果setState是同步的,那么就会导致 在某些时刻
                父组件的name值和子组件在传递进入的name值是不一致的
                这无法保证子组件的props和父组件中的对应元素的状态应时刻保持一致        
            */}
    
            <Child name={ this.state.name } />
          </div>
        )
      }
    }
    
    

2.2 如何拿到异步更新的数据

  1. setState方法中

    {/*
    	这里的异步回调函数的功能有点类似于vue中的$nextTick
    	等待界面完成更新完毕以后在拿取对应的数据
    */}
    this.setState({
        message: 'Hello React'
    }, () => console.log(this.state.message))
    
  2. componentDidUpdate

componentDidUpdate() {
    console.log(this.state.message)
}

注意: 执行顺序是优先执行componentDidUpdate这个callback中内容

在执行setState中的方法

三、 setState在某些情况下是同步更新的

setState是同步更新还是异步更新主要区分如下:

  • 组件生命周期React合成事件中,setState是异步
  • setTimeout或者原生dom事件中,setState是同步

定时器

setTimeout(() => {
    this.setState({
        message: 'Hello React'
    })
    console.log(this.state.message)
}, 0)

原生DOM事件

componentDidMount() {
    document.getElementById('btn').addEventListener('click', () => {
        this.setState({
            message: 'Hello React'
        })
        console.log(this.state.message)
    })
}

React中,每一个类组件对象都会对应一个updater对象

React会自动计算需要执行合并的事件和是异步执行还是同步执行(在React中的是BatchSync

随后由updater对象对state中的状态进行更新操作

四、 setState中的数据合并

原有数据如下:

{
    name: 'Klaus',
    age: 23
}

有一个新的数据如下:

{
    age: 18
}

使用setState合并后的数据如下

{
    name: 'Klaus',
    age: 18
}

setState中的数据合并使用的是Object.assign方法

Object.assign({}, preState, partialState)

所以其对新老state中的数据合并时候,只会覆盖同名的数据类型,且是浅拷贝

五、SetState的自身合并

在点击按钮的时候,进行三次递增操作

increment() {
    this.setState({
        count: this.state.count + 1
    })

    this.setState({
        count: this.state.count + 1
    })

    this.setState({
        count: this.state.count + 1
    })
}

wA2n1K.gif

可以看出,虽然点击按钮的时候,在代码中对state中的count递增了3次,

但是在界面中, 其实际只递增了1次

原因是 setState是异步批处理操作的

所以setState将3次递增操作合并在一起执行,又setState的合并操作是使用Object.assign方法来执行合并的,

/* 
  第一次
  Object.assign({}, {count: 0}, { count: 1 }) ---> { count: 1 }

  第二和第三次
  Object.assign({}, {count: 1}, { count: 1 }) ---> { count: 1 }
*/

但是我们希望其实现递增,而不是直接合并,那么我们可以在使用setState的时候,给其第一个参数传递一个function

increment() {
    this.setState((preState, nextProp) => ({
        count: preState.count + 1
    }))

    this.setState((preState, nextProp) => ({
        count: preState.count + 1
    }))

    this.setState((preState, nextProp) => ({
        count: preState.count + 1
    }))
}

wA2RjU.gif

setState这个方法中传入callback的时候,callback可以传入上一个的state值和props(nextProps 一般不使用)

其执行原理和不使用函数作为参数时候的执行原理是一致的,只不过使用函数作为参数的时候,可以获取上一次的state状态

随后可以通过在上一次状态基础上递增(也就是累加),从而达到批量操作的效果

/*
  第一次
  Object.assign({}, {count: 0}, { count: 1 }) ---> { count: 1 }

  第二次
  Object.assign({}, {count: 1}, { count: 2 }) ---> { count: 2 }

  第三次
  Object.assign({}, {count: 2}, { count: 3 }) ---> { count: 3 }
*/

补充

react中的合成事件对象

  • 因为react可以依据不同的reactDom运行于不同的平台上,所以其在不同的平台上的事件对象都是不同的,

  • 在浏览器端,传入的是原生的事件对象(DOM事件对象)

  • 在native端,传入的就是原生的控件对象

  • react获取了这些对象,会根据不同的平台特性对事件和事件对象进行了封装,以便于统一操作和使用

  • 这样的事件被叫做合成事件,这样的对象别叫做合成事件对象

上一篇 React组件化(下) 下一篇 React更新流程