• React中setState的怪异行为 ——setState没有即时生效


    setState可以说是React中使用频率最高的一个函数了,我们都知道,React是通过管理状态来实现对组件的管理的,当this.setState()被调用的时候,React会重新调用render方法来重新渲染UI

    但实际使用的时候,我们会发现,有时候我们setState之后,并没有立刻生效,例如我们看一下以下的示例代码

    class Test extends Component {

      constructor(props) {

        super(props);

        this.state = {

          count: 0

        };

      }

      componentDidMount() {

        this.setState({count: this.state.count + 1});

        console.log(this.state.count); // 输出0

        this.setState({count: this.state.count + 1});

        console.log(this.state.count); // 输出0

        setTimeout(() => {

          this.setState({count: this.state.count + 1});

          console.log(this.state.count); // 输出2

          this.setState({count: this.state.count + 1});

          console.log(this.state.count); // 输出3

        }, 0);

      }

      render() {

        return <div> test </div>;

      }

    }

    开发过程中我们会发现,在componentDidMount方法中,我们调用setState之后state的值并没有立即改变,但如果我们在setTimeOut里面调用,我们却能立刻就能获得更新,原因就在于react中的使用了基于事务(传送门,关于事务原理的解析)的异步更新机制,但对于这个异步的理解,又跟ajax的异步有所不同,因为毕竟react是一个js框架,所有的操作都是单线程的,所有的操作,都得按顺序来,那么它具体是怎么实现的呢?

    我们都知道,对于dom的操作对性能的损耗是非常严重的,所以react为了提高整体的渲染性能,会将一次渲染周期中的state进行合并,在这个渲染周期中你对所有setState的所有调用都会被合并起来之后,再一次性的渲染,这样可以避免频繁的调用setState导致频繁的操作dom,提高渲染性能。具体的实现方面,可以简单的理解为react中存在一个状态变量isBatchingUpdates,当处于渲染周期开始时,这个变量会被设置成true,渲染周期结束时,会被设置成falsereact会根据这个状态变量,当出在渲染周期中时,仅仅只是将当前的改变缓存起来,等到渲染周期结束时,再一次性的全部render,,具体的流程可以参照下面的流程图

    现在,我们回到最开始的问题,为什么一开始在componentDidMount中直接执行setState会无法立刻得到更新呢,原因就在于,我们在componentDidMount中其实处于首次渲染的事务当中,这次事务的渲染尚未完成,而首次渲染的时候会将isBatchingUpdates设置为true,这是我们在componentDidMount中调用setStatereact会发现当前事务尚未完成,只会直接将修改后的state放入到dirtyComponents中,等待最终渲染周期完成时,将所有的state进行合并,一次性render。而当我们放在setTimeOut里面的时候,setTimeOut会将操作放到执行队列的最后方,也就是说会等待渲染周期结束之后再进行setState,这个时候状态变量已经被重置回来了,所以此时我们的每一次setState都会立刻生效

    接下来,我们从源码的角度来看看setState是怎么操作的

    function enqueueUpdate(component) {

      ensureInjected();

      //不在渲染周期中

      if (!batchingStrategy.isBatchingUpdates) {

        batchingStrategy.batchedUpdates(enqueueUpdate, component);

        return;

      }

    //渲染周期中,直接缓存state等待下一步更新

      dirtyComponents.push(component);

    }

    这个逻辑跟上图的逻辑是一样的,当我们调用setState函数的时候,实际上最终会调用到enqueueUpdate函数,整体逻辑上面已经分析过了,就不再赘述,接下来看看setState是如何通过事务来进行渲染的,也就是batchingStrategy.batchedUpdates到底做了些什么,往下走之前,如果对事务不了解建议先看看这篇文章(传送门,关于事务原理的解析)

    var ReactUpdates = require('ReactUpdates');

    var Transaction = require('Transaction');

    var emptyFunction = require('emptyFunction');

    //第二个wrapper

    var RESET_BATCHED_UPDATES = {

      initialize: emptyFunction,

      close: function() {

        ReactDefaultBatchingStrategy.isBatchingUpdates = false;

      },

    };

    //第一个wrapper

    var FLUSH_BATCHED_UPDATES = {

      initialize: emptyFunction,

      close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),

    };

    //wrapper列表

    var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

    //事务构造函数

    function ReactDefaultBatchingStrategyTransaction() {

    //原型中定义的初始化方法

      this.reinitializeTransaction();

    }

    //继承原型

    Object.assign(

      ReactDefaultBatchingStrategyTransaction.prototype,

      Transaction.Mixin,

      {

        getTransactionWrappers: function() {

          return TRANSACTION_WRAPPERS;

        },

      }

    );

    //新建一个事务

    var transaction = new ReactDefaultBatchingStrategyTransaction();

    var ReactDefaultBatchingStrategy = {

      isBatchingUpdates: false,

      batchedUpdates: function(callback, a, b, c, d, e) {

        var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

        ReactDefaultBatchingStrategy.isBatchingUpdates = true;

        // The code is written this way to avoid extra allocations

        if (alreadyBatchingUpdates) {

          callback(a, b, c, d, e);

        } else {

          //在这个地方调用事务,callback是从外部传入的方法

          transaction.perform(callback, null, a, b, c, d, e);

        }

      },

    };

    上面这个就是react渲染所使用的事务,react就是用这个事务来处理setState引起的渲染,根据我们刚刚的解释,我们可以看到,事务开始时就把isBatchingUpdates设置成了true,防止在一次渲染周期中重复渲染,我们还可以看到这个事务定义了两个wrapper,其出口方法close分别用于执行渲染和设置状态变量,而执行渲染的FLUSH_BATCHED_UPDATES 要先于执行设置状态变量的RESET_BATCHED_UPDATES ,也就是说,执行渲染之后,才会通过RESET_BATCHED_UPDATESclose方法执行这句代码

    ReactDefaultBatchingStrategy.isBatchingUpdates = false;

    之后整个渲染周期结束。这时候当我们执行setState的时候,重新进入一个新的渲染周期

    那么,问题来了,当我们在渲染周期中执行了setState之后,我们要如何获取到最新的state的值呢,setTimeOut是一个方案,但是不太优雅,有没有其他方法呢,我们注意到,setState提供了一个回调函数,我们只需要在回调里面获取更新后的state即可,像这样

    componentDidMount() {

        this.setState({count: this.state.count + 1},()=>{

         console.log(this.state.count);//该是啥就是是啥

        }));

      }

  • 相关阅读:
    一文详解云上自动化部署集群管理工具 Nebula Operator
    手把手教你从数据预处理开始体验图数据库
    图查询语言的历史回顾短文
    集群通信:从心跳说起
    @Constraint 自定义注解校验手机号
    【工具】maven插件自动生成mapper文件
    【工具】方法日志打印+任务切片
    简单梳理下 Vue3 的新特性
    团队与领导力健康检查 | 体检表
    详细介绍Scrum Master八大职责(Scrum Master能力说明)
  • 原文地址:https://www.cnblogs.com/laneyfu/p/11341481.html
Copyright © 2020-2023  润新知