• 【React】深入理解React组件状态State


    一、何为State

    React 的核心思想是组件化,而组件中最重要的概念是State(状态),State是一个组件的UI数据模型,是组件渲染时的数据依据。
    状态(state) 和 属性(props) 类似,都是一个组件所需要的一些数据集合,但是state是私有的,可以认为state是组件的“私有属性(或者是局部属性)”。

    二、如何定义State

    定义一个合适的State,是正确创建组件的第一步。State必须能代表一个组件UI呈现的完整状态集,即组件的任何UI改变,都可以从State的变化中反映出来;同时,State还必须是代表一个组件UI呈现的最小状态集,即State中的所有状态都是用于反映组件UI的变化,没有任何多余的状态,也不需要通过其他状态计算而来的中间状态

    三、如何判断是否为State

    组件中用到的一个变量是不是应该作为组件State,可以通过下面的4条依据进行判断:

    1. 这个变量是否是通过Props从父组件中获取?如果是,那么它不是一个状态。
    2. 这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不是一个状态。
    3. 这个变量是否可以通过其他状态(State)或者属性(Props)计算得到?如果是,那么它不是一个状态。
    4. 这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性,例如组件中用到的定时器,就应该直接定义为this.timer,而不是this.state.timer。

    并不是组件中用到的所有变量都是组件的状态!当存在多个组件共同依赖一个状态时,一般的做法是状态上移,将这个状态放到这几个组件的公共父组件中。

    四、如何正确使用State

    1、用setState修改State

    直接修改state,组件并不会重新触发render()

    import React, { Component } from 'react'
    
    export default class stateStudy extends Component {
        state = {
            myText: '收藏',
        }
        render() {
            return (
                <div>
                    <h1>欢迎来到React开发</h1>
                    <button onClick={() => {
                        this.state({
                            myText: '取消收藏'
                        })
                    }}>{this.state.myText}</button>
                </div>
            )
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这时点击收藏时,报:状态不是一个函数

    在这里插入图片描述

    正确的修改方式是使用setState()

    import React, { Component } from 'react'
    
    export default class stateStudy extends Component {
        state = {
            myText: '收藏',
            myTextShow: true
        }
        render() {
            return (
                <div>
                    <h1>欢迎来到React开发</h1>
                    <button onClick={() => {
                        this.setState({
                            myTextShow: !this.state.myTextShow
                        })
                    }}>{this.state.myTextShow ? '收藏' : '取消收藏'}</button>
                </div>
            )
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    2、State的更新是异步的

    • 调用setState后,setState会把要修改的状态放入一个队列中(因而组件的state并不会立即改变)
    • 之后React会优化真正的执行时机来优化性能,所以优化过程中有可能会将多个setState的状态修改合并为一次状态修改,因而state更新可能是异步的
    • 所以不要依赖当前的state,计算下个State。当真正执行状态修改时,依赖的this.state并不能保证是最新的State,因为React会把多次State的修改合并成一次,这时,this.state将还是这几次State修改前的State

    另外需要注意:同样不能依赖当前的Props计算下个状态,因为Props一般也是从父组件的State中获取,依然无法确定在组件状态更新时的值

    ①、代码示例

    现在渲染一个button,想每点击一下,counter就+3

    class App extends React.Component {
      state = {
        counter: 0,
      }
      handleClick = () => {
        const { counter } = this.state;
        //或者 const counter = this.state.counter;
        this.setState({ counter: counter + 1 });
        this.setState({ counter: counter + 1 });
        this.setState({ counter: counter + 1 });
      }
      render() {
        return (
          <div>
            counter is: {this.state.counter}
            <button onClick={this.handleClick} >点我</button>
          </div>
        )
      }
    }
    ReactDOM.render(<App />, document.getElementById('root'));
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述

    之所以+1,不是+3,是因为state的更新可能是异步的,React会把传入多个setState的多个Object”batch“起来合并成一个。合并成一个就相当于把传入setState的多个Object进行shallow merge,像这样:

    const update = {
        counter: counter + 1,
        counter: counter + 1,
        counter: counter + 1
        //因为上面三句话都一样,所以会当一句话执行
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    想要实现+3的效果,就可以按照下面的方式进行实现:

    class App extends React.Component {
      state = {
        counter: 0,
      }
      handleClick = () => {
        this.setState(prev => ({ counter: prev.counter + 1 }));
        this.setState(prev => ({ counter: prev.counter + 1 }));
        this.setState(prev => ({ counter: prev.counter + 1 }));
        //这样是错的 this.setState(prev => {counter: prev.counter + 1});
        //这样是错的 this.setState(prev => {counter:++prev.counter});
        //这样是错的 this.setState(prev => {counter:prev.counter++});
      }
      render() {
        return (
          <div>
            counter is: {this.state.counter}
            <button onClick={this.handleClick} >点我</button>
          </div>
        )
      }
    }
    ReactDOM.render(<App />, document.getElementById('root'));
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    之所以成功是因为:传入多个 setState 的多个 Object 会被 shallow Merge,而传入多个 setState 的多个 function 会被 “queue” 起来,queue 里的 function 接收到的 state(上面是 prev )都是前一个 function 操作过的 state。

    3、State更新会被合并

    ①、组件状态例子

    this.state = {
      title : 'React',
      content : 'React is an wonderful JS library!'
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ②、当只需要修改状态title时,只需要将修改后的title传给setState

    this.setState({title: 'Reactjs'});
    
    
    • 1
    • 2

    ③、React会合并新的title到原来的组件状态中,同时保留原有的状态content

    合并后的State为:

    {
      title : 'Reactjs',
      content : 'React is an wonderful JS library!'
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4、setState里顺序更新

      //history 为数组
       this.setState({
           history: history.concat([1]),  //(1)
           current: history.length,       //(2)
           nextPlayer: !nextPlayer,       //(3)
      });
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    执行setState时:先更新history,然后再用更新改变后的history计算current的值,最后再更新nextPlayer

    五、根据State类型更新

    当状态发生变化时,如何创建新的状态?根据状态的类型,分为以下三种情况:

    1、状态的类型是不可变类型(数字,字符串,布尔值,null,undefined)

    这种情况最简单,直接给要修改的状态赋一个新值即可

    //原state
    this.state = {
      count: 0,
      title : 'React',
      success:false
    }
    //改变state
    this.setState({
      count: 1,
      title: 'bty',
      success: true
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2、状态的类型是数组

    数组是一个引用,React 执行 diff 算法时比较的是两个引用,而不是引用的对象。所以直接修改原对象,引用值不发生改变的话,React 不会重新渲染。因此,修改状态的数组或对象时,要返回一个新的数组或对象。

    ①、增加

    如有一个数组类型的状态books,当向books中增加一本书(chinese)时,使用数组的concat方法或ES6的数组扩展语法

    // 方法一:将state先赋值给另外的变量,然后使用concat创建新数组
    let books = this.state.books; 
    this.setState({
      books: books.concat(['chinese'])
    })
    
    // 方法二:使用preState、concat创建新数组
    this.setState(preState => ({
      books: preState.books.concat(['chinese'])
    }))
    
    // 方法三:ES6 spread syntax
    this.setState(preState => ({
      books: [...preState.books, 'chinese']
    }))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ②、截取

    当从books中截取部分元素作为新状态时,使用数组的slice方法:

    // 方法一:将state先赋值给另外的变量,然后使用slice创建新数组
    let books = this.state.books; 
    this.setState({
      books: books.slice(1,3)
    })
    // 
    // 方法二:使用preState、slice创建新数组
    this.setState(preState => ({
      books: preState.books.slice(1,3)
    }))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ③、条件过滤

    当从books中过滤部分元素后,作为新状态时,使用数组的filter方法:

    // 方法一:将state先赋值给另外的变量,然后使用filter创建新数组
    var books = this.state.books; 
    this.setState({
      books: books.filter(item => {
        return item != 'React'; 
      })
    })
    
    // 方法二:使用preState、filter创建新数组
    this.setState(preState => ({
      books: preState.books.filter(item => {
        return item != 'React'; 
      })
    }))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    注意:不要使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat、slice、filter会返回一个新的数组。

    3、状态的类型是普通对象(不包含字符串、数组)

    对象是一个引用,React执行diff算法时比较的是两个引用,而不是引用的对象,所以直接修改原对象,引用值不发生改变的话,React不会重新渲染。因此,修改状态的数组或对象时,要返回一个新的对象

    ①、使用ES6的Object.assgin方法

    // 方法一:将state先赋值给另外的变量,然后使用Object.assign创建新对象
    var owner = this.state.owner;
    this.setState({
      owner: Object.assign({}, owner, {name: 'Jason'})
    })
    
    // 方法二:使用preState、Object.assign创建新对象
    this.setState(preState => ({
      owner: Object.assign({}, preState.owner, {name: 'Jason'})
    }))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ②、使用对象扩展语法(Object spread properties)

    // 方法一:将state先赋值给另外的变量,然后使用对象扩展语法创建新对象
    var owner = this.state.owner;
    this.setState({
      owner: {...owner, name: 'Jason'}
    })
    
    // 方法二:使用preState、对象扩展语法创建新对象
    this.setState(preState => ({
      owner: {...preState.owner, name: 'Jason'}
    }))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    综上所述:创建新的状态对象的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。

    六、State向下流动

    我们说 props 是组件对外的接口,state 是组件对内的接口。
    一个组件可以选择将 state(状态) 向下传递,作为其子组件的 props(属性):

    <MyComponent title={this.state.title}/>
    
    
    • 1
    • 2

    这通常称为一个“从上到下”,或者“单向”的数据流。任何 state(状态) 始终由某个特定组件所有,并且从该 state(状态) 导出的任何数据 或 UI 只能影响树中 “下方” 的组件。

    如果把组件树想像为 props(属性) 的瀑布,所有组件的 state(状态) 就如同一个额外的水源汇入主流,且只能随着主流的方向向下流动。

    七、State与Props区别

    1、Props

    props是指组件间传递的一种方式,,props自然也是可以传递state的。因为React的数据流是自上而下的,所以是从父组件向子组件进行的传递,另外组件内部的this.props属性是只读的不可修改

    2、State

    state不同于props的一点是,state是可以被改变的。不过,不可以直接通过this.state=的方式来修改,而是需要通过this.setState()方法来修改state

    3、二者区别

    • props是传递给组件的(类似于函数的形参)、是不可修改的
    • state是在组件内被组件自己管理的(类似于在一个函数内声明的变量),是多变的、可修改的,每次setState都异步更新

    八、Props的使用

    当一个组件被注入一些属性(Props )值时,属性值来源于它的父级元素,所以人们常说,属性在 React 中是单向流动的:从父级到子元素

    1、props(属性)默认为“true”

    如果你没给 prop(属性) 传值,那么他默认为 true 。下面两个 JSX 表达式是等价的:

    <MyTextBox autocomplete />
    <MyTextBox autocomplete={true} />
    
    
    • 1
    • 2
    • 3

    通常情况下,我们不建议使用这种类型,因为这会与ES6中的对象shorthand混淆 。ES6 shorthand 中 {foo} 指的是 {foo: foo} 的简写,而不是 {foo: true} 。这种行为只是为了与 HTML 的行为相匹配。

    举个例子:在 HTML 中,< input type=“radio” value=“1” disabled /> 与 < input
    type=“radio” value=“1” disabled=“true” /> 是等价的。JSX 中的这种行为就是为了匹配 HTML
    的行为。

    2、props扩展

    如果你已经有一个 object 类型的 props,并且希望在 JSX 中传入,你可以使用扩展操作符 … 传入整个 props 对象。这两个组件是等效的:

    function App1() {
      return <Greeting firstName="Ben" lastName="Hector" />;
    }
    
    function App2() {
      const props = {firstName: 'Ben', lastName: 'Hector'};
      return <Greeting {...props} />;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    显然下面的方法更方便:因为它将数据进行了包装,而且还简化了赋值的书写

  • 相关阅读:
    java加sqlite3右键菜单版圣经,还没检查错误
    spring cloud day(7) config+bus
    Vim编辑器常用命令汇总
    vue3 快速入门系列 —— 状态管理 pinia
    (121)DAC接口--->(006)基于FPGA实现DAC8811接口
    [附源码]java毕业设计病历管理系统
    金和OA系统,C6版本,公文流程里面的附件,在服务器打开是乱码,进行了加密,求解密
    上N下P三极管推挽电路----》交越失真问题的解决
    windbg 调试基本配置
    java并发编程:CopyOnWrite容器介绍
  • 原文地址:https://blog.csdn.net/weixin_45490023/article/details/133999638