react-复习(1)setState到底是异步还是同步?

2,178 阅读4分钟

先给出答案:有时是同步,有时是异步。 setState在合成事件和生命周期函数里是异步的,在原生事件和setTimeout里是同步的

一、合成事件和生命周期函数里是异步的

我们可以看一个🌰:

export default class App extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0,
    };
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        <h1>{count}</h1>
        <button id="add" onClick={this.btnChange}>+</button>
      </div>
    );
  }
  btnChange = () => {
    this.setState({
      count: this.state.count + 1,
    });
    console.log("this.state.count :>> ", this.state.count);
  };
}

这个时候我们点击按钮,打印的state的值是0,不是最新的1,

其实造成setState的异步并不是由内部的异步代码引起的,在本身的执行过程中时同步的,但是合成事件和生命周期函数的调用顺序在更新之前,导致在内部不能直接得到更新后的值。我们可以利用setState的第二个参数callback得到最新的值,代码如下:

  btnChange = () => {
    this.setState({
      count: this.state.count + 1,
    }, () => {
      //此时打印为 1
      console.log("this.state.count :>> ", this.state.count);
    });
    //此时打印为 0
    console.log("this.state.count :>> ", this.state.count);
  };

需要值得注意的是在控制台中,先打印0,后打印1。

在组件的生命周期中同理,也是异步的。

二、在原生事件和setTimeout里是同步的

而在原生的DOM时间和setTimeout中,则表现为同步,我们将上面的点击事件做一下修改

  btnChange = () => {
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1,
      });
      console.log("this.state.count :>> ", this.state.count);
    }, 0);
  };

此时,打印的为更新后的count数值

同理在原生的DOM事件中也是一样的:

  componentDidMount() {
    document.querySelector("#add").addEventListener("click", () => {
      this.setState({
        count: this.state.count + 1,
      });
      console.log("this.state.count :>> ", this.state.count);
    });
  }

三、异步setState可能会被合并的问题

同时,异步的setState中还有一个问题,就是进行多次相同的异步setState操作时,更新前会被合并。

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

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

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

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

答案显然意见,四次打印的都是 1

我们在按钮的点击事件函数中进行了四次相同的setState;需要值得注意的是,使用setState处理state的时候,并不是直接修改原来的state,而是通过setState创建一个对象,让原来的state进行处理后赋值给新的state,在本例中即count。最后将新的state和原来的state进行合并。在对象中,我们对同一个属性进行多次同样的赋值,结果肯定会被合并,并不会出现state的值被对此修改。所以上面打印的结果是四次1,而不是1,2,3,4。

那我们怎么让它不进行合并呢?

我们可以在setState中使用回调函数的形式返回一个对象:

  btnChange = () => {
    this.setState((state,props) => {
      return {
        count: state.count + 1,
      };
    },() => {
      console.log('this.state.count :>> ', this.state.count);
    });
    this.setState((state,props) => {
      return {
        count: state.count + 1,
      };
    },() => {
      console.log('this.state.count :>> ', this.state.count);
    });
    this.setState((state,props) => {
      return {
        count: state.count + 1,
      };
    },() => {
      console.log('this.state.count :>> ', this.state.count);
    });
    this.setState((state,props) => {
      return {
        count: state.count + 1,
      };
    },() => {
      console.log('this.state.count :>> ', this.state.count);
    });
  };

这个回调函数接受两个参数,一个是当前的state值,另一个是props,执行return {}时,count值被返回给了state中,然后再执行第二个setState,以此类推,当异步操作执行完之后,再执行setState中的callback函数,此时count的值已经是最新的4了,因此打印四次4。我们虽然看不到它从1到4打印的结果,但这个流程我们需要清楚。

而在同步的setState中,则不会进行合并

  btnChange = () => {
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1,
      },() => {
        console.log("this.state.count :>> ", this.state.count);
      });
    });
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1,
      },() => {
        console.log("this.state.count :>> ", this.state.count);
      });
    });
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1,
      },() => {
        console.log("this.state.count :>> ", this.state.count);
      });
    });
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1,
      },() => {
        console.log("this.state.count :>> ", this.state.count);
      });
    });
  };

我们可以看到打印的结果符合我们的预期:

四、结语

最后讲一下我对于React不让直接修改state的原因。记得PureComponentshouldcomponentupdate么,常用它们进行性能优化。它的作用就是用于拦截组件渲染。比较之前的stateprops跟更新后的stateprops进行浅比较,从而决定是否渲染该组件。如果我们直接修改state的值,那这样的比较就没有意义了,一直是相同的,也就无从说起性能优化了。更别说如果state的值一直不变,React根本就不会重新执行render了。

由于笔者还是个前端小萌新,难免会有纰漏错误,欢迎大佬们进行指点!