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马上生效。

 

引用原文:http://blog.csdn.net/sysuzhyupeng/article/details/63250741

 

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,文章可以转载,无需版权。希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家一起探讨,代码如有问题,欢迎各位大神指正!

posted @ 2017-09-19 14:11  緈諨の約錠  阅读(840)  评论(0编辑  收藏  举报
//用户复制动作发生0.1秒以后再去改粘贴板中的内容。100ms只是一个经常取值,写1000、10、50、200……都行。