React 中 setState() 更新机制和源码解读

作者: 贺鹏飞 分类: React,数据可视化 发布时间: 2021-01-28 22:39

对于 setState() 相信伙伴们都用过,它是 React 官方推荐用来更新组件 state 的 API,但是对于 setState() 你真的了解吗?在网上看了很多关于this.setState()的介绍,觉得受益匪浅,就总结了一些帮助自己理解的点,在此分享出来。

setState() 官方用法指南

语法1: setState(updater,[callback])

  • updater:函数类型,返回一个更新后的 state 中的状态对象,它会和 state 进行浅合并。
  • callback: 可选,回调函数。

语法2: setState(stateChange,[callback])

  • setState: 对象类型,会将传入的对象浅层合并到新的 state 中。
  • callback:可选,回调函数。

对于这两种形式,不同的是第一个参数选择问题,可以选择一个函数返回一个新的state对象,亦可以直接选择一个对象应用于状态更新,那么啥时候选择函数类型的参数,什么时候选择对象类型的呢?这里可以总结两句话:

  • 当前更新状态依赖之前的状态时,选择函数类型参数;
  • 当前状态更新无需依赖之前的state状态时,选择对象类型参数。

setState() 更新机制

我们知道setState() 会触发组件render() 函数,重新渲染组件将更新后的内容显示在视图上,那么在 setState() 之后我们立马就能获取到最新的state值吗?

这里涉及到一个 setState() 是异步更新还是同步更新的问题?

结论:

  • 在React相关的回调函数中setState() 是异步更新
  • 不在React 相关的回调中setState() 是同步更新

React 相关的回调包括:组件的生命周期钩子,React 组件事件监听回调。

React不相关的回调包括常见的:setTimeout(), Promise()等。

我们拿按钮点击实例来测试:

import React from 'react';
class Count extends React.Component {
    state = {
        count: 0
    }
    text1 = () => {
        this.setState({
            count: this.state.count+1
        })
        console.log(this.state.count)
    }
    text2 = () => {
        setTimeout(() => {
            this.setState(state => ({
                count: state.count+1
            }))
            console.log(this.state.count)
        })
    }
    text3 = () => {
        Promise.resolve().then(value => {
            this.setState({
                count: 7
            })
            console.log(this.state.count)
        })
    }
    componentWillMount () {
        this.setState(state => ({
            count: state.count+1
        }))
        console.log(this.state.count)
    }
    render () {
        console.log('render()', this.state.count)
        return (
            <div>
                <h1>{this.state.count}</h1>
                <button onClick={this.text1} style={{marginRight: 15}}>测试1</button>
                <button onClick={this.text2} style={{marginRight: 15}}>测试2</button>
                <button onClick={this.text3}>测试3</button>
            </div>         
        )
    }     
  }
export default Count;

React 事件监听回调 text1 和 组件生命周期 componentWillMount() 钩子里面分别在setState()之后打印最新的 state 值,发现打印出来的还是修改之前的state,但是页面已经更新为最新状态:
componentWillMount() 输出顺序是: 0,和 render() 1,属于异步更新;
点击“测试1”按钮输出的顺序是:1和render()2,属于异步更新;

“测试2”按钮的setTimeout() 输出的顺序是:render() 2 和 2,属于同步更新;
“测试3”按钮的 Promise() 回调中输出的顺序是:render() 7 和 7,属于同步更新。

setState 为什么会同步和异步更新组件?

进入这个问题之前,我们先回顾一下现在对 setState 的认知:

  1. setState 不会立刻改变React组件中state的值。
  2. setState 通过触发一次组件的更新来引发重绘。
  3. 多次 setState 函数调用产生的效果会合并。

重绘指的就是引起 React 的更新生命周期函数4个函数:

  • shouldComponentUpdate(被调用时this.state没有更新;如果返回了false,生命周期被中断,虽然不调用之后的函数了,但是state仍然会被更新)
  • componentWillUpdate(被调用时this.state没有更新)
  • render(被调用时this.state得到更新)
  • componentDidUpdate

如果每一次 setState 调用都走一圈生命周期,光是想一想也会觉得会带来性能的问题,其实这四个函数都是纯函数,性能应该还好,但是render函数返回的结果会拿去做Virtual DOM比较和更新DOM树,这个就比较费时间。

setState批量更新的过程

如下图所示:

Batch Update 即「批量更新」。在 MV* 框架中,Batch Update 可以理解为将一段时间内对 model 的修改批量更新到 view 的机制。

在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中。
而 isBatchingUpdates 默认是 false,也就表示 setState 会同步更新 this.state,但是有一个函数 batchedUpdates。
这个函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state。

通过上图,我们知道了大致流程, 要想彻底了解它的机制,我们解读一下源码。

setState 源码解读

// setState方法入口如下:
ReactComponent.prototype.setState = function (partialState, callback) {
    // 将setState事务放入队列中
    this.updater.enqueueSetState(this, partialState);
    if (callback) {
      this.updater.enqueueCallback(this, callback, 'setState');
    }
};
//partialState,有部分state的含义,可见只是影响涉及到的state,不会伤及无辜。
//enqueueSetState 是 state 队列管理的入口方法,比较重要,我们之后再接着分析。

replaceState :

replaceState: function (newState, callback) {
    this.updater.enqueueReplaceState(this, newState);
    if (callback) {
      this.updater.enqueueCallback(this, callback, 'replaceState');
    }
}
//replaceState中取名为newState,有完全替换的含义。同样也是以队列的形式来管理的。

enqueueSetState:

enqueueSetState: function (publicInstance, partialState) {
    // 先获取ReactComponent组件对象
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    if (!internalInstance) {
      return;
    }
    // 如果_pendingStateQueue为空,则创建它。可以发现队列是数组形式实现的
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);
    // 将要更新的ReactComponent放入数组中
    enqueueUpdate(internalInstance);
}

getInternalInstanceReadyForUpdate

function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
    // 从map取出ReactComponent组件,还记得mountComponent时把ReactElement作为key,将ReactComponent存入了map中了吧,ReactComponent是React组件的核心,包含各种状态,数据和操作方法。而ReactElement则仅仅是一个数据类。
    var internalInstance = ReactInstanceMap.get(publicInstance);
    if (!internalInstance) {
      return null;
    }
   return internalInstance;
}

enqueueUpdate:

function enqueueUpdate(component) {
    ensureInjected();
    // 如果不是正处于创建或更新组件阶段,则处理update事务
    if (!batchingStrategy.isBatchingUpdates) {
      batchingStrategy.batchedUpdates(enqueueUpdate, component);
      return;
    }
    // 如果正在创建或更新组件,则暂且先不处理update,只是将组件放在dirtyComponents数组中
    dirtyComponents.push(component);
}

batchedUpdates:

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,后面详细分析transaction
      transaction.perform(callback, null, a, b, c, d, e);
    }
}
var RESET_BATCHED_UPDATES = {
    initialize: emptyFunction,
    close: function () {
      // 事务批更新处理结束时,将isBatchingUpdates设为了false
      ReactDefaultBatchingStrategy.isBatchingUpdates = false;
    }
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

enqueueUpdate包含了React避免重复render的逻辑。mountComponent 和 updateComponent 方法在执行的最开始,会调用到 batchedUpdates 进行批处理更新,此时会将isBatchingUpdates设置为true,也就是将状态标记为现在正处于更新阶段了。

之后React以事务的方式处理组件update,事务处理完后会调用wrapper.close() 。而TRANSACTION_WRAPPERS 中包含了RESET_BATCHED_UPDATES 这个wrapper,故最终会调用RESET_BATCHED_UPDATES.close(), 它最终会将isBatchingUpdates设置为false

故 getInitialStatecomponentWillMount, rendercomponentWillUpdate 中 setState 都不会引起 updateComponent。但在componentDidMount 和 componentDidUpdate中则会。

通过wrapper进行封装事务

一个wrapper包含一对 initialize 和 close 方法。比如 RESET_BATCHED_UPDATES

var RESET_BATCHED_UPDATES = {
    // 初始化调用
    initialize: emptyFunction,
    // 事务执行完成,close时调用
    close: function () {
      ReactDefaultBatchingStrategy.isBatchingUpdates = false;
    }
};

ranscation被包装在wrapper中,比如:

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

transaction 是通过transaction.perform(callback, args…)方法进入的,它会先调用注册好的wrapper 中的initialize方法,然后执行perform方法中的callback,最后再执行close方法。

transaction.perform(callback, args…):

initializeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    // 遍历所有注册的wrapper
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
        // 调用wrapper的initialize方法
        this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
      } finally {
        if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
          try {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  }

closeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    // 遍历所有wrapper
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        errorThrown = true;
        if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
          // 调用wrapper的close方法,如果有的话
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          try {
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  }

更新组件: runBatchedUpdates

前面分析到enqueueUpdate中调用transaction.perform(callback, args...)后,发现,callback还是enqueueUpdate方法啊,那岂不是死循环了?不是说好的setState会调用updateComponent,从而自动刷新View的吗? 我们还是要先从transaction事务说起。

我们的wrapper中注册了两个wrapper,如下:

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_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会在一个transactionclose阶段运行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数组,主要干两件事。

  1. 首先执行performUpdateIfNecessary来刷新组件的view
  2. 执行之前阻塞的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);
    }
}

最后惊喜的看到了receiveComponentupdateComponent吧。

receiveComponent最后会调用updateComponent,而updateComponent中会执行React组件存在期的生命周期方法,如:componentWillReceiveProps, shouldComponentUpdate, componentWillUpdaterendercomponentDidUpdate。从而完成组件更新的整套流程。

整体流程回顾:

  1. enqueueSetState将state放入队列中,并调用enqueueUpdate处理要更新的Component
  2. 如果组件当前正处于update事务中,则先将Component存入dirtyComponent中。否则调用batchedUpdates处理。
  3. batchedUpdates发起一次transaction.perform()事务
  4. 开始执行事务初始化,运行,结束三个阶段
  5. 初始化:事务初始化阶段没有注册方法,故无方法要执行
  6. 运行:执行setSate时传入的callback方法,一般不会传callback参数
  7. 结束:更新isBatchingUpdates为false,并执行FLUSH_BATCHED_UPDATES这个wrapper中的close方法
  8. FLUSH_BATCHED_UPDATES在close阶段,会循环遍历所有的dirtyComponents,调用updateComponent刷新组件,并执行它的pendingCallbacks, 也就是setState中设置的callback。

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注