setState详解

Posted 浴盆

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了setState详解相关的知识,希望对你有一定的参考价值。

我们都知道,React通过this.state来访问state,通过this.setState()方法来更新state。当this.setState()方法被调用的时候,React会重新调用render方法来重新渲染UI

setState异步更新

setState方法通过一个队列机制实现state更新,当执行setState的时候,会将需要更新的state合并之后放入状态队列,而不会立即更新this.state(可以和浏览器的事件队列类比)。如果我们不使用setState而是使用this.state.key来修改,将不会触发组件的re-render。如果将this.state赋值给一个新的对象引用,那么其他不在对象上的state将不会被放入状态队列中,当下次调用setState并对状态队列进行合并时,直接造成了state丢失。(这里特别感谢@Dcatfly的指正)
我们来看一下React文档中对setState的说明

    void setState(
      function|object nextState,
      [function callback]
    )

The second (optional) parameter is a callback function that will be executed once setState is completed and the component is re-rendered.
翻译一下,第二个参数是一个回调函数,在setState的异步操作结束并且组件已经重新渲染的时候执行。也就是说,我们可以通过这个回调来拿到更新的state的值。
React也正是利用状态队列机制实现了setState的异步更新,避免频繁地重复更新state(pending的意思是未定的,即将发生的)

   //将新的state合并到状态更新队列中
   var nextState =  this._processPendingState(nextProps, nextContext);
   //根据更新队列和shouldComponent的状态来判断是否需要更新组件
   var shouldUpdate = 
      this._pendingForceUpdate ||
      !inst.shouldComponentUpdate ||
      inst.shouldComponentUpdate(nextProps, nextState, nextContext);

setState循环调用风险

当调用setState时,实际上会执行enqueueSetState方法,并对partialState以及_pending-StateQueue更新队列进行合并操作,最终通过enqueueUpdate执行state更新
而performUpdateIfNecessary方法会获取_pendingElement,_ pendingStateQueue,_ pending-ForceUpdate,并调用receiveComponent和updateComponent方法进行组件更新
如果在shouldComponentUpdate或者componentWillUpdate方法中调用setState,此时this._pending-StateQueue != null,就会造成循环调用,使得浏览器内存占满后崩溃

调用栈

既然setState最终是通过enqueueUpdate执行state更新,那么enqueueUpdate到底是如何更新state的呢? 首先看下面的问题

   import React,  Component  from 'react';
   class Example extends Component 
       constructor()
           super();
           //在组件初始化可以直接操作this.state
           this.state = 
               val: 0
           
       
       componentDidMount()
           this.setState(
              val: this.state.val + 1
           );
           //第一次输出
           console.log(this.state.val);
           this.setState(
              val: this.state.val + 1
           );
           //第二次输出
           console.log(this.state.val);
           setTimeout(()=>
              this.setState(val: this.state.val + 1);
               //第三次输出
               console.log(this.state.val);
               this.setState(
                  val: this.state.val + 1
               );
               //第四次输出
               console.log(this.state.val);
           , 0);  
       
       render()
           return null;
       
   

上述代码中,4次console.log打印出来的val分别是: 0,0,2 ,3
我们来看一个简化的setState的调用栈

   this.setState(newState) =>
   newState存入pending队列 =>
   调用enqueueUpdate =>
   是否处于批量更新模式 =>
   是的话将组件保存到dirtyComponents
   不是的话遍历dirtyComponents,调用updateComponent,更新pending state or props

enqueueUpdate的源码如下(上面流程的第三步)(batching的意思是批量的)

   function enqueueUpdate(component)
       //injected注入的
       ensureInjected();
       //如果不处于批量更新模式
       if(!batchingStrategy.isBatchingUpdates)
           batchingStrategy.batchedUpdates(enqueueUpdate, component);
           return;
       
       //如果处于批量更新模式
       dirtyComponents.push(component);
   

如果isBatchingUpdates为true,则对所有队列中的更新执行batchedUpdates方法,否则只把当前组件(即调用了setState的组件)放入dirtyComponents数组中,例子中4次setState调用的表现之所以不同,这里的逻辑判断起了关键作用

事务

事务就是将需要执行的方法使用wrapper封装起来,再通过事务提供的perform方法执行,先执行wrapper中的initialize方法,执行完perform之后,在执行所有的close方法,一组initialize及close方法称为一个wrapper。
那么事务和setState方法的不同表现有什么关系,首先我们把4次setState简单归类,前两次属于一类,因为它们在同一调用栈中执行,setTimeout中的两次setState属于另一类。
在setState调用之前,已经处在batchedUpdates执行的事务中了。那么这次batchedUpdates方法是谁调用的呢,原来是ReactMount.js中的_renderNewRootComponent方法。也就是说,整个将React组件渲染到DOM中的过程就是处于一个大的事务中。而在componentDidMount中调用setState时,batchingStrategy的isBatchingUpdates已经被设为了true,所以两次setState的结果没有立即生效。
再反观setTimeout中的两次setState,因为没有前置的batchedUpdates调用,所以导致了新的state马上生效。

以上是关于setState详解的主要内容,如果未能解决你的问题,请参考以下文章

setState详解

setState详解

React setState注意事项

当 setState 与类的对象实例发生反应时会发生啥

深入React技术栈之setState详解

Java中setState(true)一般啥意思