【深入理解React】07_理解React 15中的setState

前言

你觉得setState是同步的还是异步的?

先举一个例子,在页面上有三个按钮:+1, + 3和-1,依次点击三个按钮,猜测输出结果:

import * as React from 'react';
import { Component } from 'react';
import './App.css'
import { AppWrapper } from './style'

class App extends Component{
  state = {
    count: 0
  }

  increment = () => {
    console.log('增加1之前', this.state);
    this.setState({
      count: this.state.count + 1
    })
    console.log('增加1之后', this.state);
  }

  incrementThree = () => {
    console.log('增加3之前', this.state);
    this.setState({
      count: this.state.count + 1
    })
    this.setState({
      count: this.state.count + 1
    })
    this.setState({
      count: this.state.count + 1
    })
    console.log('增加3之后', this.state);
  }

  reduce = () => {
    setTimeout(() => {
      console.log('减少1之前', this.state);
      this.setState({
        count: this.state.count - 1
      })
      console.log('减少1之后', this.state);
    })
  }

  render(){
    return (
      <>
        <button onClick={this.increment}>点击增加1</button>
        <button onClick={this.incrementThree}>点击增加3</button>
        <button onClick={this.reduce}>点击减少1</button>
      </>
    )
  }
}

export default App

在这里插入图片描述

设计动机

从生命周期角度来看,当组件调用setState更新数据时,会依次触发下面的钩子函数:

在这里插入图片描述

当我们多次调用setState时,就会重复调用这个更新流程,即re-render,将会有很大的性能开销,所以为了避免的re-rendersetState就设计成了异步。React真正运行时,每来一个setState,就把它塞进一个队列里“攒起来”,等时机成熟,再将缓存的state进行合并,最后只对最新的state值走一次更新流程。这个过程叫做“批量更新”。

从源码解读setState的工作流

先来看一张流程图:

在这里插入图片描述

setState

先在react中找到setState:

ReactComponent.prototype.setState = function (partialState, callback) {
  // 将setState事务放入enqueueSetState队列中
  this.updater.enqueueSetState(this, partialState);
  // 将setState的回调函数放入enqueueCallback放入队列中
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

partialState有部分state的含义,即只是影响state,不会伤及无辜。

enqueueSetState

enqueueSetState: function (publicInstance, partialState) {
    // 先获取ReactComponent组件对象
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

    if (!internalInstance) {
      return;
    }

    // queue即对应该组件实例的 state 数组
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    // 将要更新的ReactComponent放入数组中
    enqueueUpdate(internalInstance);
}

enqueueUpdate

function enqueueUpdate(component) {
  ensureInjected();

  // 用isBatchingUpdates来判断当前是否处于批量创建/更新组件的阶段
  // 如果不是正处于创建或更新组件阶段,则立即更新组件
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  // 如果正在创建或更新组件,则暂且先不处理,只是将组件放在dirtyComponents数组中,稍后再处理
  dirtyComponents.push(component);
}

isBatchingUpdates=true表明当前处于更新事务状态中,将component存入dirtyComponent,否则调用batchedUpdates,发起一个transaction.perform

isBatchingUpdates(批量更新策略)

var ReactDefaultBatchingStrategy = {	
    isBatchingUpdates: false,
    // 发起更新动作
    batchedUpdates: function (callback, a, b, c, d, e) {
      // 缓存锁变量
      var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
      // 批处理最开始时,将isBatchingUpdates设为true,表明正在更新(上锁)
      ReactDefaultBatchingStrategy.isBatchingUpdates = true;

      // The code is written this way to avoid extra allocations
      if (alreadyBatchingUpdates) {
        callback(a, b, c, d, e);
      } else {
        // 以事务的方式处理updates,把callback放进transaction中,后面详细分析transaction
        transaction.perform(callback, null, a, b, c, d, e);
      }
    }
}

事务transaction

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

Transaction事务说白了就是在不改变原有方法的基础上,在执行方法的前后进行额外的操作。具体来说,就是一个方法会被wrapper包裹,且方法需要通过perform来调用,且在被包裹方法的前后分别执行initializeclose

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    // 事务批更新处理结束时,将isBatchingUpdates设为了false
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

RESET_BATCHED_UPDATES中的code阶段会将isBatchingUpdates设置为false,而FLUSH_BATCHED_UPDATES中的close会执行flushBatchedUpdates,里面包含了Vitual DOM到真实DOM的映射等其他操作,即更新DOM。

flushBatchedUpdates

这个方法会遍历所有的dirtyComponents,更新组件主要调用了performUpdateIfNecessary,又卷入到了事务中。

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  // 进行一个排序操作,这里因为通常情况下,父组件更新后,子组件也会随之更新
  // 所以这里进先进行排序,使得子组件在父组件之前被更新
  dirtyComponents.sort(mountOrderComparator);

  // 代表批量更新的次数, 保证每个组件只更新一次
  updateBatchNumber++;
  // 遍历 dirtyComponents
  for (var i = 0; i < len; i++) {
    var component = dirtyComponents[i];
      
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
    ...
    // 执行更新
    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction,
      updateBatchNumber,
    );
    ...
    // 存储 callback以便后续按顺序调用
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance(),
        );
      }
    }
  }
}

performUpdateIfNecessary

ReactReconciler会调用组件实例的performUpdateIfNecessary. 如果接收了props, 就会调用此组件的receiveComponent, 再在里面调用updateComponent更新组件; 如果没有接受props, 但是有新的要更新的状态(_pendingStateQueue不为空)就会直接调用updateComponent来更新:

performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement != null) {
        ReactReconciler.receiveComponent(this, this._pendingElement, transaction,                 this._context);
    // 在组件更新时,this._prndingStateQueue = true时,就会一直调用updateComponent。
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
        this.updateComponent(transaction, this._currentElement, this._currentElement,             this._context, this._context);
    } else {
        this._updateBatchNumber = null;
    }
}

updateComponent

该方法中,主要使用_processPendingStatestate进行了操作

_processPendingState: function (props, context) {
    var inst = this._instance;    // _instance保存了Constructor的实例,即通过ReactClass创建的组件的实例
    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;
  }
  • 如果更新队列为null,则返回原来的state
  • 如果更新队列有一个值,则返回更新值;
  • 如果更新队列有多个值,则for循环将它们合并。

由此可见,在一个生命周期中所有的state变化都会被合并并统一处理。

另外updateComponent中还会在合并state之后、新state映射到DOM之前,先调用shouldComponentUpdate()来判断是否更新组件。

setState流程图

在这里插入图片描述

在调用setState时,会先判断是否处于批量创建/更新组件的阶段,如果不是,再进入设置isBatchingUpdatestrue,并执行事务机制进行更新;如果刚好处于批量创建/更新组件的过程中,则存入dirtyComoponent,然后遍历dirtyComponent循环更新组件,有卷入到了事务处理中。等事务处理完成isBatchingUpdatesfalse,然后再shouldComponentUpdate前合并state,通过shouldComponentUpdate判断组件是否要更新。

常见的错误使用方式

使用this.state.xxx = xxx更新

从上面可知setState通过一个队列实现state更新。当执行setState时,会将需要更新的state合并后放入状态队列,而不会立刻更新this.state,队列可以高效地批量更新state。如果直接修改this.state,那么该state将不会被放入状态队列中,即会被忽略。

在shouldComponentUpdate或componentWillUpdate更新state

从上面源码的分析我们可以总结出setState的流程,当我们更新组件时,组件中由于使用setState也会进行state的合并,当state合并成功之后,则对组件进行更新。组件更新调用了updateComponent方法,该方法会合并state,并跟shouluComponetUpdate来判断是否更新直接。

如果我们在shouldComponentUpdata或者componentWillUpdate方法中调用setState,此时this._pendingStateQueue != null,则perfromUpdateIfNessary就会调用updateComponent进行组件更新,但是updateComponent方法又会调用shouldComponentUpdatecomponentWillUpdate方法,因此造成循环引用,使得浏览器内存占满而奔溃。

这也可以理解为什么React 16中为什么要废除这两个钩子了。

输出结果解释

最前面那个例子我们可以理解成:

  • 当我们点击按钮时,由于此时已经存在在一个大事务中(不必深究),此时isBatchingUpdate=true,所以incrementincrementThree两次setState都没有立即生效,而是放置在了dirtyComponents中,新的state等待合并和更新到组件中,所以this.state.couut = 0
  • setTimoutout的逻辑时异步执行的,此时内部的setState执行时isBatchingUpdate=false,所以state立即更新。

总结

从上面可知:

  • setState并不能保证同步更新,但是也不能说它的更新方式是异步的。在React钩子函数及合成事件中,它表现为异步,但在setTimeout、setInterval等函数中,它表现为同步;
  • setState实现异步的机制是为了防止用户多次setState导致频繁的re-render
  • 由于组件更新会调用updateComponent,在合并state之后会调用shouldComponentUpdate,所以我们不能在shouldComponentUpdate或者componentWillUpdate中使用setState,不然会导致死循环;
  • 使用this.state.xxx = xxx更新数据并不会加入到state的更新队列中,即无法更新数据。

参考


如有错误,欢迎指出,感谢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值