美文网首页
【React进阶系列】 setState机制

【React进阶系列】 setState机制

作者: 这个前端不太冷 | 来源:发表于2020-05-25 17:21 被阅读0次
    timg (4).jpg

    api解析: setState(updater, [callback])


    updater: 更新数据 FUNCTION/OBJECT
    callback: 更新成功后的回调 FUNCTION
    
    // updater - Function
    this.setState((prevState, props) => {
      return {counter: prevState.counter + props.step};
    });
    
    // update - Object
    this.setState({quantity: 2})
    

    setState的特点:


    1.异步:react通常会集齐一批需要更新的组件,然后一次性更新来保证渲染的性能
    2.浅合并 Objecr.assign()
    
    

    setState问题与解决

    举个🌰


    • 在使用setState改变状态之后,立刻通过this.state去拿最新的状态
      解决: componentDidUpdate或者setState的回调函数里获取
    // setState回调函数
    changeTitle: function (event) {
      this.setState({ title: event.target.value }, () => this.APICallFunction());
    },
    APICallFunction: function () {
      // Call API with the updated value
    }
    
    • 有一个需求,需要在在onClick里累加两次,使用对象的方法更新,则只会加一次
      解决: 使用updater function
    onClick = () => {
        this.setState({ index: this.state.index + 1 });
        this.setState({ index: this.state.index + 1 });
    }
    
    // 最后解析为,后面的数据会覆盖前面的更改,所以最终只加了一次.
    Object.assign(
      previousState,
      {index: state.index+ 1},
      {index: state.index+ 1},
    )
    
    //正确写法
    onClick = () => {
        this.setState((prevState, props) => {
          return {quantity: prevState.quantity + 1};
        });
        this.setState((prevState, props) => {
          return {quantity: prevState.quantity + 1};
        });
    }
    

    注意:

    1.不要在render()函数里面写setstate(),除非你自己定制了shouldComponentUpdate方法,要不然会引起无限循环

    render() {
        //this.setState
        return(
            //...dom
        )
    }
    

    2.不要给this.state直接复制
    react为了实现高效render, state其实是一个队列,setState是将数据插入队列中,使用方式1直接赋值不会触发渲染, react提供了setState的实例方法可以触发render。

    // 1
    this.state.num = 1
    // 2
    this.setState({
        num: this.state.num + 1
    })
    

    3.对数组和对象等引用对象操作时,使用返回新对象的方法
    array: 不要使用push、pop、shift、unshift、splice可使用concat、slice、filter、扩展语法
    object: Object.assgin/扩展语法

    setState更新机制


    如图:


    3114633915-5c7108d209e99_articlex.png

    图不清楚可以点击查看原图

    • partialStatesetState传入的第一个参数,对象或函数
    • _pendingStateQueue:当前组件等待执行更新的state队列
    • isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用
    • dirtyComponent:当前所有处于待更新状态的组件队列
    • transcation:react的事务机制,在被事务调用的方法外包装n个waper对象,并一次执行:waper.init、被调用方法、waper.close
    • FLUSH_BATCHED_UPDATES:用于执行更新的waper,只有一个close方法

    2.执行过程

    对照上面流程图的文字说明,大概可分为以下几步:

    • 1.将setState传入的partialState参数存储在当前组件实例的_pendingStateQueue中。
    • 2.判断当前React是否处于批量更新状态,如果是,将当前组件标记为dirtyCompontent,并加入待更新的组件队列中。
    • 3.如果未处于批量更新状态,将isBatchingUpdates设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。
    • 4.调用事务的waper方法,遍历待更新组件队列依次执行更新。
    • 5.执行生命周期componentWillReceiveProps
    • 6.将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将_pendingStateQueue置为空。
    • 7.执行生命周期shouldComponentUpdate,根据返回值判断是否要继续更新。
    • 8.执行生命周期componentWillUpdate
    • 9.执行真正的更新,render
    • 10.执行生命周期componentDidUpdate

    setState源码世界

    相信能到这里的同学都知道了setState()是个既能同步又能异步的方法了,那具体什么时候是同步的,什么时候是异步的?

    去源码里面看实现是比较靠谱的方式。

    1、如何快速查看react源码

    上react的github仓库,直接clone下来

    react-github仓库

    git clone https://github.com/facebook/react.git
    

    到目前我看为止,最新的版本是16.13.1,我选了15.6.0的代码

    如何切换版本?

    1、找到对应版本号

    image.png

    2、复制15.6.0的历史记录号

    image.png

    3、回滚

    git reset --hard 911603b
    

    如图,成功回滚到15.6.0版本

    image.png

    2、setState入口 => enqueueSetState

    核心原则:既然是看源码,那当然就不是一行一行的读代码,而是看核心的思想,所以接下来的代码都只会放核心代码,旁枝末节只提一下或者忽略setState的入口文件在src/isomorphic/modern/class/ReactBaseClasses.jsReact组件继承自React.Component,而setState是React.Component的方法,因此对于组件来讲setState属于其原型方法

    ReactComponent.prototype.setState = function(partialState, callback) {
      this.updater.enqueueSetState(this, partialState);
      if (callback) {
        this.updater.enqueueCallback(this, callback, 'setState');
      }
    };
    

    partialState顾名思义-“部分state”,这取名,大概就是想不影响原来的state的意思吧
    当调用setState时实际上是调用了enqueueSetState方法,我们顺藤摸瓜(我用的是vscode的全局搜索),找到了这个文件src/renderers/shared/stack/reconciler/ReactUpdateQueue.js

    image.png

    这个文件导出了一个ReactUpdateQueue对象,“react更新队列”,代码名字起的好可以自带注释,说的就是这种大作吧,在这里注册了enqueueSetState方法

    3、enqueueSetState => enqueueUpdate

    先看enqueueSetState的定义

    enqueueSetState: function(publicInstance, partialState) {
        var internalInstance = getInternalInstanceReadyForUpdate(
          publicInstance,
          'setState',
        );
        
        var queue =
          internalInstance._pendingStateQueue ||
          (internalInstance._pendingStateQueue = []);
        queue.push(partialState);
    
        enqueueUpdate(internalInstance);
      },
    

    这里只需要关注internalInstance的两个属性:

    • _pendingStateQueue:待更新队列
    • _pendingCallbacks: 更新回调队列
      如果_pendingStateQueue的值为null,将其赋值为空数组[],并将partialState放入待更新state队列_pendingStateQueue,最后执行enqueueUpdate(internalInstance)

    接下来看enqueueUpdatefunction

    enqueueUpdate(internalInstance) {
      ReactUpdates.enqueueUpdate(internalInstance);
    }
    

    它执行的是ReactUpdates的enqueueUpdate方法

    var ReactUpdates = require('ReactUpdates');
    

    这个文件刚好就在旁边src/renderers/shared/stack/reconciler/ReactUpdates.js。找到enqueueUpdate方法

    var ReactUpdates = {
      /**
       * React references `ReactReconcileTransaction` using this property in order
       * to allow dependency injection.
       *
       * @internal
       */
      ReactReconcileTransaction: null,
    
      batchedUpdates: batchedUpdates,
      enqueueUpdate: enqueueUpdate,
      flushBatchedUpdates: flushBatchedUpdates,
      injection: ReactUpdatesInjection,
      asap: asap,
    };
    
    module.exports = ReactUpdates;
    

    定义如下

    function enqueueUpdate(component) {
      ensureInjected();
    
      if (!batchingStrategy.isBatchingUpdates) {
        batchingStrategy.batchedUpdates(enqueueUpdate, component);
        return;
      }
    
      dirtyComponents.push(component);
      if (component._updateBatchNumber == null) {
        component._updateBatchNumber = updateBatchNumber + 1;
      }
    }
    

    这段代码对于理解setState非常重要

    if (!batchingStrategy.isBatchingUpdates) {
        batchingStrategy.batchedUpdates(enqueueUpdate, component);
        return;
      }
    dirtyComponents.push(component);
    

    判断batchingStrategy.isBatchingUpdates。batchingStrategy是批量更新策略,isBatchingUpdates表示是否处于批量更新过程,开始默认值为false

    上面这句话的意思是:

    如果处于批量更新模式,也就是isBatchingUpdates为true时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中;如果不处于批量更新模式,对所有队列中的更新执行batchedUpdates方法,往下看下去就知道是用事务的方式批量的进行component的更新,事务在下面。

    借用《深入React技术栈》Page167中一图

    image.png

    4、核心:batchedUpdates => 调用transaction

    batchingStrategy.isBatchingUpdates又是怎么回事呢?看来它才是关键.

    但是,batchingStrategy 对象并不好找,它是通过 injection 方法注入的,一番寻找,发现了 batchingStrategy 就是ReactDefaultBatchingStrategy。 src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js具体怎么找文件,又属于另一个范畴了,我们今天只专注 setState,其他的容后再说吧

    相信部分同学看到这里已经有些迷糊了,没关系,再坚持一下,旁枝末节先不管,只知道我们找到了核心方法batchedUpdates,马上要胜利了,别放弃(我第一次看也是这样熬过来的,一遍不行就两遍,大不了看多几遍又如何)

    先看批量更新策略-batchingStrategy,它到底是什么

    var ReactDefaultBatchingStrategy = {
      isBatchingUpdates: false,
    
      batchedUpdates: function(callback, a, b, c, d, e) {
        var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    
        ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    
        if (alreadyBatchingUpdates) {
          return callback(a, b, c, d, e);
        } else {
          return transaction.perform(callback, null, a, b, c, d, e);
        }
      },
    };
    
    module.exports = ReactDefaultBatchingStrategy;
    

    终于找到了,isBatchingUpdates属性和batchedUpdates方法如果isBatchingUpdates为true,当前正处于更新事务状态中,则将Component存入dirtyComponent中,否则调用batchedUpdates处理,发起一个transaction.perform()

    注:所有的 batchUpdate 功能都是通过执行各种 transaction 实现的这是事务的概念,先了解一下事务吧

    5、Transaction(事务)

    这一段就直接引用书本里面的概念吧,《深入React技术栈》Page169

    image.png

    简单地说,一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠加。

    具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。

    下面这段代码应该能帮助理解

    var Transaction = require('./Transaction');
    
    // 我们自己定义的 Transaction
    var MyTransaction = function() {
      // do sth.
      this.reinitializeTransaction();
    };
    
    Object.assign(MyTransaction.prototype, Transaction.Mixin, {
      getTransactionWrappers: function() {
        return [{
          initialize: function() {
            console.log('before method perform');
          },
          close: function() {
            console.log('after method perform');
          }
        }];
      };
    });
    
    var transaction = new MyTransaction();
    var testMethod = function() {
      console.log('test');
    }
    transaction.perform(testMethod);
    
    // before method perform
    // test
    // after method perform
    

    看了上面的代码,如果还没有了解transaction.。没关系。可以看一下这篇文章,写的非常详细
    React transaction完全解读

    6、核心分析:batchingStrategy 批量更新策略

    回到batchingStrategy:批量更新策略,再看看它的代码实现

    var ReactDefaultBatchingStrategy = {
      isBatchingUpdates: false,
    
      batchedUpdates: function(callback, a, b, c, d, e) {
        var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    
        ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    
        if (alreadyBatchingUpdates) {
          return callback(a, b, c, d, e);
        } else {
          return transaction.perform(callback, null, a, b, c, d, e);
        }
      },
    };
    

    可以看到isBatchingUpdates的初始值是false的,在调用batchedUpdates方法的时候会将isBatchingUpdates变量设置为true。然后根据设置之前的isBatchingUpdates的值来执行不同的流程

    还记得上面说的很重要的那段代码吗

    if (!batchingStrategy.isBatchingUpdates) {
        batchingStrategy.batchedUpdates(enqueueUpdate, component);
        return;
      }
    dirtyComponents.push(component);
    

    1、首先,点击事件的处理本身就是在一个大的事务中(这个记着就好),isBatchingUpdates已经是true了

    2、调用setState()时,调用了ReactUpdates.batchedUpdates用事务的方式进行事件的处理

    3、在setState执行的时候isBatchingUpdates已经是true了,setState做的就是将更新都统一push到dirtyComponents数组中;

    4、在事务结束的时候才通过 ReactUpdates.flushBatchedUpdates 方法将所有的临时 state merge 并计算出最新的 props 及 state,然后将批量执行关闭结束事务。

    到这里我并没有顺着ReactUpdates.flushBatchedUpdates方法讲下去,这部分涉及到渲染和Virtual Dom的内容,反正你知道它是拿来执行渲染的就行了。

    到这里为止,setState的核心概念已经比较清楚了,再往下的内容,暂时先知道就行了,不然展开来讲一环扣一环太杂了,我们做事情要把握核心。

    到这里不知道有没有同学想起一个问题

    isBatchingUpdates 标志位在 batchedUpdates 发起的时候被置为 true ,那什么时候被复位为false的呢?

    还记得上面的事务的close方法吗,同一个文件src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

    var transaction = new ReactDefaultBatchingStrategyTransaction();
    // 定义复位 wrapper
    var RESET_BATCHED_UPDATES = {
      initialize: emptyFunction,
      close: function () {
        ReactDefaultBatchingStrategy.isBatchingUpdates = false;
      }
    };
    
    // 定义批更新 wrapper
    var FLUSH_BATCHED_UPDATES = {
      initialize: emptyFunction,
      close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
    };
    
    var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
    
    function ReactDefaultBatchingStrategyTransaction() {
      this.reinitializeTransaction();
    }
    
    _assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
      getTransactionWrappers: function () {
        return TRANSACTION_WRAPPERS;
      }
    });
    

    相信眼尖的同学已经看到了,close的时候复位,把isBatchingUpdates设置为false。

    image.png
    Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
      getTransactionWrappers: function() {
        return TRANSACTION_WRAPPERS;
      },
    });
    
    var transaction = new ReactDefaultBatchingStrategyTransaction();
    

    通过原型合并,事务的close 方法,将在 enqueueUpdate 执行结束后,先把 isBatchingUpdates 复位,再发起一个 DOM 的批更新

    到这里,我们会发现,前面所有的队列、batchUpdate等等都是为了来到事务的这一步,前面都只是批收集的工作,到这里才真正的完成了批更新的操作。

    当然在实际代码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。

    说了这么多 Transaction,关于上文提到的RESET_BATCHED_UPDATES主要用来管理isBatchingUpdates状态这句话是不是;理解更透彻了呐?

    上文提到了两个wrapper:RESET_BATCHED_UPDATES和FLUSH_BATCHED_UPDATES。RESET_BATCHED_UPDATES用来管理isBatchingUpdates状态,我们前面在分析setState是否立即生效时已经讲解过了。那FLUSH_BATCHED_UPDATES用来干嘛呢?

    var FLUSH_BATCHED_UPDATES = {
      initialize: emptyFunction,
      close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
    };
    
    var flushBatchedUpdates = function () {
      // 循环遍历处理完所有dirtyComponents
      while (dirtyComponents.length || asapEnqueued) {
        if (dirtyComponents.length) {
          var transaction = ReactUpdatesFlushTransaction.getPooled();
          // close前执行完runBatchedUpdates方法,这是关键
          transaction.perform(runBatchedUpdates, null, transaction);
          ReactUpdatesFlushTransaction.release(transaction);
        }
    
        if (asapEnqueued) {
          asapEnqueued = false;
          var queue = asapCallbackQueue;
          asapCallbackQueue = CallbackQueue.getPooled();
          queue.notifyAll();
          CallbackQueue.release(queue);
        }
      }
    };
    
    

    FLUSH_BATCHED_UPDATES会在一个transaction的close阶段运行runBatchedUpdates,从而执行update。

    function runBatchedUpdates(transaction) {
      var len = transaction.dirtyComponentsLength;
      dirtyComponents.sort(mountOrderComparator);
    
      for (var i = 0; i < len; i++) {
        // dirtyComponents中取出一个component
        var component = dirtyComponents[i];
    
        // 取出dirtyComponent中的未执行的callback,下面就准备执行它了
        var callbacks = component._pendingCallbacks;
        component._pendingCallbacks = null;
    
        var markerName;
        if (ReactFeatureFlags.logTopLevelRenders) {
          var namedComponent = component;
          if (component._currentElement.props === component._renderedComponent._currentElement) {
            namedComponent = component._renderedComponent;
          }
        }
        // 执行updateComponent
        ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
    
        // 执行dirtyComponent中之前未执行的callback
        if (callbacks) {
          for (var j = 0; j < callbacks.length; j++) {
            transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
          }
        }
      }
    }
    
    

    runBatchedUpdates循环遍历dirtyComponents数组,主要干两件事。首先执行performUpdateIfNecessary来刷新组件的view,然后执行之前阻塞的callback。下面来看performUpdateIfNecessary。

    performUpdateIfNecessary: function (transaction) {
      if (this._pendingElement != null) {
        // receiveComponent会最终调用到updateComponent,从而刷新View
        ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
      }
    
      if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
        // 执行updateComponent,从而刷新View。这个流程在React生命周期中讲解过
        this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
      }
    }
    

    最后惊喜的看到了receiveComponent和updateComponent吧。receiveComponent最后会调用updateComponent,而updateComponent中会执行React组件存在期的生命周期方法,如componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate,render, componentDidUpdate。 从而完成组件更新的整套流程。

    updateComponent: function(
        transaction,
        prevParentElement,
        nextParentElement,
        prevUnmaskedContext,
        nextUnmaskedContext,
      ) {
        var inst = this._instance;
        invariant(
          inst != null,
          'Attempted to update component `%s` that has already been unmounted ' +
            '(or failed to mount).',
          this.getName() || 'ReactCompositeComponent',
        );
    
        var willReceive = false;
        var nextContext;
    
        // Determine if the context has changed or not
        if (this._context === nextUnmaskedContext) {
          nextContext = inst.context;
        } else {
          nextContext = this._processContext(nextUnmaskedContext);
          willReceive = true;
        }
    
        var prevProps = prevParentElement.props;
        var nextProps = nextParentElement.props;
    
        // Not a simple state update but a props update
        if (prevParentElement !== nextParentElement) {
          willReceive = true;
        }
    
        // An update here will schedule an update but immediately set
        // _pendingStateQueue which will ensure that any state updates gets
        // immediately reconciled instead of waiting for the next batch.
        if (willReceive && inst.componentWillReceiveProps) {
            inst.componentWillReceiveProps(nextProps, nextContext);
        }
    
        var nextState = this._processPendingState(nextProps, nextContext);
        var shouldUpdate = true;
    
        if (!this._pendingForceUpdate) {
          if (inst.shouldComponentUpdate) {
             shouldUpdate = inst.shouldComponentUpdate(
                nextProps,
                nextState,
                nextContext,
              );
          } else {
            if (this._compositeType === CompositeTypes.PureClass) {
              shouldUpdate =
                !shallowEqual(prevProps, nextProps) ||
                !shallowEqual(inst.state, nextState);
            }
          }
        }
        this._updateBatchNumber = null;
        if (shouldUpdate) {
          this._pendingForceUpdate = false;
          // Will set `this.props`, `this.state` and `this.context`.
          this._performComponentUpdate(
            nextParentElement,
            nextProps,
            nextState,
            nextContext,
            transaction,
            nextUnmaskedContext,
          );
        } else {
          // If it's determined that a component should not update, we still want
          // to set props and state but we shortcut the rest of the update.
          this._currentElement = nextParentElement;
          this._context = nextUnmaskedContext;
          inst.props = nextProps;
          inst.state = nextState;
          inst.context = nextContext;
        }
      },
    
      _processPendingState: function(props, context) {
        var inst = this._instance;
        var queue = this._pendingStateQueue;
        var replace = this._pendingReplaceState;
        this._pendingReplaceState = false;
        this._pendingStateQueue = null;
    
        if (!queue) {
          return inst.state;
        }
    
        if (replace && queue.length === 1) {
          return queue[0];
        }
    
        var nextState = Object.assign({}, replace ? queue[0] : inst.state);
        for (var i = replace ? 1 : 0; i < queue.length; i++) {
          var partial = queue[i];
          Object.assign(
            nextState,
            typeof partial === 'function'
              ? partial.call(inst, nextState, props, context)
              : partial,
          );
        }
    
        return nextState;
      },
    

    这一部分代码相对来说不算是很难,replace是存在是由于之前被废弃的APIthis.replaceState,我们现在不需要关心这一部分,现在我们可以回答刚开始的问题,为什么给setState传入的参数是函数时,就可以解决刚开始的例子。

    Object.assign(
        nextState,
        typeof partial === 'function' ?
            partial.call(inst, nextState, props, context) :
            partial
    );
    

    如果我们传入的是对象

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

    我们现在已经知道,调用setState是批量更新,那么第一次调用之后,this.state.value的值并没有改变。两次更新的value值其实是一样的,所以达不到我们的目的。但是如果我们传递的是回调函数的形式,那么情况就不一样了,partial.call(inst, nextState, props, context)接受的state都是上一轮更新之后的新值,因此可以达到我们预期的目的。 
       
    _processPendingState在计算完新的state之后,会_performComponentUpdate:

    function _performComponentUpdate(
        nextElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        unmaskedContext
      ) {
        var inst = this._instance;
    
        var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
        var prevProps;
        var prevState;
        var prevContext;
        if (hasComponentDidUpdate) {
          prevProps = inst.props;
          prevState = inst.state;
          prevContext = inst.context;
        }
    
        if (inst.componentWillUpdate) {
          inst.componentWillUpdate(nextProps, nextState, nextContext);
        }
    
        this._currentElement = nextElement;
        this._context = unmaskedContext;
        inst.props = nextProps;
        inst.state = nextState;
        inst.context = nextContext;
    
        this._updateRenderedComponent(transaction, unmaskedContext);
    
        if (hasComponentDidUpdate) {
          transaction.getReactMountReady().enqueue(
            inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
            inst
          );
        }
    }
    

    我们可以看到,这部分内容涉及到了几方面内容,首先在更新前调用了钩子函数componentWillUpdate,然后更新了组件的属性(props、state、context),执行函数_updateRenderedComponent,最后再次执行钩子函数componentDidUpdate。

    _updateRenderedComponent执行组件的render方法。
    在文件/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js中,代码如下:

     /**
       * Call the component's `render` method and update the DOM accordingly.
       *
       * @param {ReactReconcileTransaction} transaction
       * @internal
       */
      _updateRenderedComponent: function(transaction, context) {
        var prevComponentInstance = this._renderedComponent;
        var prevRenderedElement = prevComponentInstance._currentElement;
        var nextRenderedElement = this._renderValidatedComponent();
    
        var debugID = 0;
    
        if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
          ReactReconciler.receiveComponent(
            prevComponentInstance,
            nextRenderedElement,
            transaction,
            this._processChildContext(context),
          );
        } else {
          var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
          ReactReconciler.unmountComponent(prevComponentInstance, false);
    
          var nodeType = ReactNodeTypes.getType(nextRenderedElement);
          this._renderedNodeType = nodeType;
          var child = this._instantiateReactComponent(
            nextRenderedElement,
            nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
          );
          this._renderedComponent = child;
    
          var nextMarkup = ReactReconciler.mountComponent(
            child,
            transaction,
            this._hostParent,
            this._hostContainerInfo,
            this._processChildContext(context),
            debugID,
          );
          this._replaceNodeWithMarkup(
            oldHostNode,
            nextMarkup,
            prevComponentInstance,
          );
        }
      },
    
      /**
       * Overridden in shallow rendering.
       *
       * @protected
       */
      _replaceNodeWithMarkup: function(oldHostNode, nextMarkup, prevInstance) {
        ReactComponentEnvironment.replaceNodeWithMarkup(
          oldHostNode,
          nextMarkup,
          prevInstance,
        );
      },
    
      /**
       * @protected
       */
      _renderValidatedComponentWithoutOwnerOrContext: function() {
        var inst = this._instance;
        var renderedElement;
    
        renderedElement = inst.render();
        return renderedElement;
      },
    

    到目前为止,我们已经基本介绍完了setState的更新过程,只剩一个部分没有介绍,那就是setState执行结束之后的回调函数。我们知道,setState函数中如果存在callback,则会有:

    if (callback) {
        this.updater.enqueueCallback(this, callback);
    }
    

    call函数会被传递给this.updater的函数enqueueCallback,然后非常类似于setState,callback会存储在组件内部实例中的_pendingCallbacks属性之中。我们知道,回调函数必须要setState真正完成之后才会调用,那么在代码中是怎么实现的。大家还记得在函数flushBatchedUpdates中有一个事务ReactUpdatesFlushTransaction:

    //代码有省略
    var flushBatchedUpdates = function() {
      while (dirtyComponents.length) {
        if (dirtyComponents.length) {
          //从事务pool中获得事务实例
          var transaction = ReactUpdatesFlushTransaction.getPooled();
          transaction.perform(runBatchedUpdates, null, transaction);
          //释放实例
          ReactUpdatesFlushTransaction.release(transaction);
        }
        //......
      }
    };
    

    我们现在看看ReactUpdatesFlushTransaction的wrapper是怎么定义的:

    var UPDATE_QUEUEING = {
      initialize: function() {
        this.callbackQueue.reset();
      },
      close: function() {
        this.callbackQueue.notifyAll();
      },
    };
    

    我们看到在事务的close阶段定义了this.callbackQueue.notifyAll(),即执行了回调函数,通过这种方法就能保证回调函数一定是在setState真正完成之后才执行的。到此为止我们基本已经解释了setState大致的流程是怎样的,但是我们还是没有回答之前的一个问题,为什么下面的两种代码会产生不同的情况:

    //未按预期执行
    _addValue() {
        this.setState({
            value: this.state.value + 1
        })
        this.setState({
            value: this.state.value + 1
        })
    }
    //按预期执行
    _addValue() {
        setTimeout(()=>{
            this.setState({
                value: this.state.value + 1
            });
            this.setState({
                value: this.state.value + 1
            });
        },0)
    }
    

    这个问题,其实真的要追本溯源地去讲,是比较复杂的,我们简要介绍一下。在第一种情况下,如果打断点追踪你会发现,在第一次执行setState前,已经触发了一个 batchedUpdates,等到执行setState时已经处于一个较大的事务,因此两个setState都是会被批量更新的(相当于异步更新的过程,thi.state.value值并没有立即改变),执行setState只不过是将两者的partialState传入dirtyComponents,最后再通过事务的close阶段的flushBatchedUpdates方法去执行重新渲染。但是通过setTimeout函数的包装,两次setState都会在click触发的批量更新batchedUpdates结束之后执行,这两次setState会触发两次批量更新batchedUpdates,当然也会执行两个事务以及函数flushBatchedUpdates,这就相当于一个同步更新的过程,自然可以达到我们的目的,这也就解释了为什么React文档中既没有说setState是同步更新或者是异步更新,只是模糊地说到,setState并不保证同步更新。

    举个🌰

    u=4000503827,2702628120&fm=26&gp=0.jpg

    如下代码:

    class App extends React.Component {
      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 <div>{this.state.val}</div>
      }
    }
    
    // 结果就为 0, 0, 2, 3
    
    • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
    • setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和- 钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
    • setState的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
      所以基于上述结论,如果想要实现上述代码中 4 次 console.log 打印出来的 val 分别是1、2、3、4。可以实现如下:
    setTimeout(() => {
        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
    
        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);
    

    或者

    this.setState((prevState) => {
        return { count: prevState.val + 1 }
    })
    console.log(this.state.val);  // 1
    
    this.setState((prevState) => {
        return { count: prevState.val + 1 }
    })
    console.log(this.state.val);  // 2
    
    this.setState((prevState) => {
        return { count: prevState.val + 1 }
    })
    console.log(this.state.val);  // 3
    
    this.setState((prevState) => {
        return { count: prevState.val + 1 }
    })
    console.log(this.state.val);  // 4
    

    setState 干了什么

    1、合成事件中的setState

    react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。
    在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。
    按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。
    当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState。

    class App extends Component {
    
      state = { val: 0 }
    
      increment = () => {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val) // 输出的是更新前的val --> 0
      }
    
      render() {
        return (
          <div onClick={this.increment}>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }
    
    

    2、生命周期函数中的setState

    整个生命周期中就是一个事物操作,所以标识位isBatchingUpdates = true,所以流程到了enqueueUpdate()时,实例对象都会加入到dirtyComponents 数组中

    class App extends Component {
    
      state = { val: 0 }
    
     componentDidMount() {
        this.setState({ val: this.state.val + 1 })
       console.log(this.state.val) // 输出的还是更新前的值 --> 0
     }
      render() {
        return (
          <div>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }
    

    3、原生事件中的setState

    原生事件是指非react合成事件,原生自带的事件监听 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 这种绑定事件的形式都属于原生事件
    原生事件绑定不会通过合成事件的方式处理,自然也不会进入更新事务的处理流程。setTimeout也一样,在setTimeout回调执行时已经完成了原更新组件流程,不会放入dirtyComponent进行异步更新,其结果自然是同步的。

    class App extends Component {
    
      state = { val: 0 }
    
      changeValue = () => {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val) // 输出的是更新后的值 --> 1
      }
    
     componentDidMount() {
        document.body.addEventListener('click', this.changeValue, false)
     }
    
      render() {
        return (
          <div>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }
    

    4、setTimeout中的setState

    由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。

    在生命周期,根据event loop的模型,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。

    class App extends Component {
    
      state = { val: 0 }
    
     componentDidMount() {
        setTimeout(_ => {
          this.setState({ val: this.state.val + 1 })
          console.log(this.state.val) // 输出更新后的值 --> 1
        }, 0)
     }
    
      render() {
        return (
          <div>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }
    
    

    5、批量更新

    在 setState 的时候react内部会创建一个 updateQueue ,通过 firstUpdate 、 lastUpdate 、 lastUpdate.next 去维护一个更新的队列,在最终的 performWork 中,相同的key会被覆盖,只会对最后一次的 setState 进行更新
    分别执行以下代码:

      componentDidMount() {
        this.setState({ index: this.state.index + 1 }, () => {
          console.log(this.state.index);
        })
        this.setState({ index: this.state.index + 1 }, () => {
          console.log(this.state.index);
        })
      }
      componentDidMount() {
        this.setState((preState) => ({ index: preState.index + 1 }), () => {
          console.log(this.state.index);
        })
        this.setState(preState => ({ index: preState.index + 1 }), () => {
          console.log(this.state.index);
        })
      }
    

    执行结果:

    1
    1
    2
    2
    

    说明:

    1.直接传递对象的setstate会被合并成一次
    2.使用函数传递state不会被合并

    批量更新中State合并机制

    我们看下流程中_processPendingState的代码,这个函数是用来合并state暂存队列的,最后返回一个合并后的state。

      _processPendingState: function (props, context) {
        var inst = this._instance;
        var queue = this._pendingStateQueue;
        var replace = this._pendingReplaceState;
        this._pendingReplaceState = false;
        this._pendingStateQueue = null;
    
        if (!queue) {
          return inst.state;
        }
    
        if (replace && queue.length === 1) {
          return queue[0];
        }
    
        var nextState = _assign({}, replace ? queue[0] : inst.state);
        for (var i = replace ? 1 : 0; i < queue.length; i++) {
          var partial = queue[i];
          _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
        }
    
        return nextState;
      },
    

    我们只需要关注下面这段代码:

    _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
    

    如果传入的是对象,很明显会被合并成一次:

    Object.assign(
      nextState,
      {index: state.index+ 1},
      {index: state.index+ 1}
    )
    

    如果传入的是函数,函数的参数preState是前一次合并后的结果,所以计算结果是准确的。

    总结

    setState流程还是很复杂的,设计也很精巧,避免了重复无谓的刷新组件。它的主要流程如下:

    1. enqueueSetState将state放入队列中,并调用enqueueUpdate处理要更新的Component;

    2.如果组件当前正处于update事务中,则先将Component存入dirtyComponent中。否则调用batchedUpdates处理。

    3.batchedUpdates发起一次transaction.perform()事务;

    4.开始执行事务初始化,运行,结束三个阶段;

    初始化:事务初始化阶段没有注册方法,故无方法要执行;
    运行:执行setSate时传入的callback方法,一般不会传callback参数;
    结束:更新isBatchingUpdates为false,并执行FLUSH_BATCHED_UPDATES这个wrapper中的close方法。
    5.FLUSH_BATCHED_UPDATES在close阶段,会循环遍历所有的dirtyComponents,调用updateComponent刷新组件,并执行它的pendingCallbacks, 也就是setState中设置的callback。

    相关文章

      网友评论

          本文标题:【React进阶系列】 setState机制

          本文链接:https://www.haomeiwen.com/subject/wdgyahtx.html