React中state 的三个注意事项

586 阅读3分钟

React 官方文档给的三点描述:

  1. 不要直接修改state
  2. state的更新可能是异步的
  3. state的更新会被合并

一、不要直接修改state

文档并没有解释原因,只说明构造函数中是唯一可以给 state 直接赋值的地方。

可能的原因:

  • 收集状态改变 数据驱动型的框架模式: 状态改变 ---> 视图自动改变。

改变状态的一般操作是直接赋值 ;另一种就是函数setxxx。

Vue 就是利用Object.defineProperty、proxy监听对象,实现状态收集。

react中并不存在依赖收集,因此,直接赋值并不能让React监听到state的变化。

  • 更好的维护数据不可变性(immutable)。 setState内部实际会返回一个新的state。如果直接修改state,会带来很多棘手的副作用(可读性变差、不可预期bug)。
// 最常见可变 就是引用赋值
const person = {player: {name: '456'}};
const person1 = person;
person.player.name = 123

// var ==》 let const 也是在强调不可变。减少var 带来的各种副作用

只有纯的没有副作用的函数,才是合格的函数。

  • 统一去更新state,利于维护和控制; 固定的setState的函数,会让它的整个渲染/更新过程变得更加可控,包括实施各类性能优化(diff、持久化数据结构)。

二、state的更新可能是异步的

异步的?那算宏任务、微任务范畴?

案例测试

案例:

handleClick() {
    const base = 5
    setTimeout(() => {
      console.log('宏任务触发')
    }, 0)
    Promise.resolve().then(()=>{
      console.log('微任务触发')
    })
    this.setState({
      count: this.state.count + base
    }, () => {
      console.log('更新后的值:', this.state.count)
    })
    console.log('methods end');
}
  // 执行结果, 说明setState 先于微任务
handleClick2() {
    setTimeout(()=>{
      const base = 5
      setTimeout(() => {
        console.log('宏任务触发2')
      }, 0)
      Promise.resolve().then(()=>{
        console.log('微任务触发2')
      })
      this.setState({
        count: this.state.count + base
      }, () => {
        console.log('更新后的值2:', this.state.count)
      })
      console.log('methods end2');
    }, 10)
}
// 执行结构  setState 同步
"更新后的值2:" 5
"methods end2"
"微任务触发2 "
"宏任务触发2"

setState本身并不是异步,只是因为react的性能优化机制处理后,体现为异步

小结论:

  • 在组件生命周期或 React 合成事件中,setState 是异步; 在 React 的生命周期以及绑定的事件流中,所有的 setState 操作会先缓存到一个队列中,在事件结束后,才会取出队列,触发state 更新。
  • 在 setTimeout 或者原生 dom 事件中,setState 是同步;

三、state的更新会被合并

State 变更意味着组件会重新render。为了优化性能,React会收集state的变更,多次调用的情况下,React只会执行一次render。

案例:

 // 初识count 为0
 mergeState() {
    for ( let i = 0; i < 10; i++ ) {
        this.setState( { count: this.state.count + 1 } , () => {
          console.log(this.state.count)
        });
    }
    // count 为1
    for ( let i = 0; i < 10; i++ ) {
        this.setState( prevState => {
            // prevSate 上一个状态
            console.log( prevState.count );
            return {
                count: prevState.count + 1
            }
        } );
    }
    // count 为10
  }
  
// 说明:
// 1.直接传递对象的setstate会被合并成一次
// 2.使用函数传递state不会被合并

最后一个demo:

  // 初始val number 0
  demo(){
    this.setState({val: this.state.val + 1});
    console.log('==第一次输出==', this.state.val); // 第 1 次输出
    this.setState({val: this.state.val + 1});
    console.log('==第二次输出==', this.state.val); // 第 2 次输出
    setTimeout(() => {
        this.setState({val: this.state.val + 1});
        console.log('==第三次输出==', this.state.val); // 第 3 次输出
        this.setState({val: this.state.val + 1});
        console.log('==第四次输出==', this.state.val); // 第 4 次输出
    }, 0);
  }
  // 0 0 2 3

《深入 React 技术栈》 图 3-15

image.png

源码

解密 setState