1、React之setState

发布于 2022年 04月 02日 06:42

       在React 16.8版本发布之前,只有类组件可以在构造函数中显示设置state来表示组件的内部状态,然后使用setState()来更新state[reactjs.org/docs/state-…]。

其中: 

       1)state只能以object形式表示

       2)直接使用this.state.propName = 'xxxx',修改propName的值,不会重新渲染组件

       3)出于性能考虑,React会将每个setState() 调用依次放入一个队列,然后再一次渲染中再依次执行相应的setState() ,所以每个setState() 的调用会将该时刻state与props的值以闭包[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures]形式保存,所以多次调用setState修改state对象的某一属性的值,可能会意料之外与期望不一致的结果(官方原文:出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用)。

       4)setState会使用Object.assign(target, ...source)的方式进行浅合并---基本类型进行值复制,复杂类型进行引用复制。

// index.js

import React from 'react' 
import ReactDOM from 'react'

ReactDOM.render(<Counter />, document.getElementById('root))

// Counter.js

import React, { Component } from 'react' 
class Counter extends Component {  
    constructor(props) {    
        super(props)    
        this.state = { count: 0 }  
    }  

    tick = () => {    
        this.state.count = this.state.count + 1
        console.log(this.state.count)  
      }

    render() {    
        const { count } = this.state    
        const style = {marginTop: '120px', marginLeft: '10px'} 
        return (      
            <div>        
                <button onClick={this.tick} style={style}> Click { count } Times</button>      
            </div>)
    }
}

export default Counter

分别点击1次,点击2次,结果如下:

                                        

可以看出:

       直接使用this.state.propName = 'xxxx',可以修改state属性的值,但组件不会重新渲染

// Counter.js

import React, { Component } from 'react' 
class Counter extends Component {  
    constructor(props) {    
        super(props)    
        this.state = { count: 0 }  
    }  

    tick = () => {    
        this.setState({ 
            count: this.state.count + 1 
        })
        console.log(this.state.count)  
      }

    render() {    
        const { count } = this.state   
        const style = {marginTop: '120px', marginLeft: '10px'}          
        return (      
            <div>        
                <button onClick={this.tick} style={style}> Click { count } Times</button>      
            </div>)
    }
}

export default Counter

分别点击1次,点击2次,点击3次,结果如下:

    

可以看出: 

       使用this.setState修改state的属性的值,就能使组件重新渲染,但是又会出现一个有趣的现象:打印count的值总是组件重新渲染前的上一次count的值,这是由于setState是异步执行的,console.log(this.state.count)会在重新渲染前先执行。而这可以通过给this.setState传入一个回调函数作为第二个参数解决该问题。

// Counter.js

import React, { Component } from 'react' 
class Counter extends Component {  
    constructor(props) {    
        super(props)  
        this.state = { count: 0 }  
    }      tick = () => {    
        this.setState({ count: this.state.count + 1 }, () => {
             console.log(this.state.count)  
        })
    }

    render() {    
        const { count } = this.state  
        const style = {marginTop: '120px', marginLeft: '10px'} 
        return (      
            <div>        
                <button onClick={this.tick} style={style}> Click { count } Times</button>      
            </div>)
    }
}

export default Counter

分别点击1次,点击2次,结果如下:

       

       所以,如果想在state更新后在执行某些操作,可以传入回调函数来执行这些操作。

但是这样还会有另一个问题:如果在某一事件处理函数中多次调用setState,而state中的属性值只会更新一次,注意: 更新一次,但是每一次传入的回调函数会放入相应的事件队列,在完全更新后依次执行。

// Counter.js

import React, { Component } from 'react' 
class Counter extends Component {  
    constructor(props) {    
        super(props)    
        this.state = { count: 0 }  
    }  

    tick = () => {    
        this.setState({ count: this.state.count + 1 }, () => {
            console.log(this.state.count)
        })      }

    tickFive = () => {
        this.tick()
        this.tick()        
        this.tick()        
        this.tick()        
        this.tick()
    }

    render() {    
        const { count } = this.state 
        const style = {marginTop: '120px', marginLeft: '10px'}            return (      
            <div>        
                <button onClick={this.tickFive} style={style}> Click { count } Times</button>      
            </div>)
    }
}

export default Counter

分别点击1次,点击2次, 结果如下:

     

可以看到:会出现两个很有意思的现象: 1)尽管在点击1次tickFive中调用了5次setState,但是界面却显示只点击了1次,点击2次tickFive中调用了10次setState,界面却显示只点击了2次 ...; 2) 5次setState回调函数里 console.log(this.state.count)的值都是一样的。为什么呢?这其实是由于JS语言的特性之一----闭包以及setState异步执行综合作用的结果。每一次点击tickFive,里面的tick会调用5次setState,而tick里每个setState里传入的参数{count: this.state.count}this.state.count里的值都是该环境里的state的属性count的值,即Class Counter的实例state里的count值。不好理解?看看下面的一道经典面试题,你就会恍然大悟。(PS:鄙人刚开始学习React,暂时没有阅读react源码,完全是从浏览器对于执行JS的执行,以及JS语言方面在理解,如理解有误方面,欢迎批评与指出。后续有时间会阅读源码,了解更多,并保持更新)

既然如此,那怎么解决呢?

答案就是将传入setState的第一个参数由对象替换成一个updater函数: (state, props) => stateChange  [reactjs.org/docs/react-…]

终极方案

// Counter.js

import React, { Component } from 'react' 
class Counter extends Component {  
    constructor(props) {    
        super(props)    
        this.state = { count: 0 }  
    }  

    tick = () => {    
        this.setState(prevState => ({ count: prevState.count + 1 }), () => {
            console.log(this.state.count)
        })      
    }
    
    tickFive = () => {
        this.tick()
        this.tick()        
        this.tick()        
        this.tick()        
        this.tick()
    }    render() {   
        const { count } = this.state    
        const style = {marginTop: '120px', marginLeft: '10px'} 
        return (      
            <div>        
                <button onClick={this.tickFive} style={style}> Click { count } Times</button>      
            </div>)
    }
}

export default Counter

 点击1次, 结果如下: 

推荐文章