对React setState的一些思考与心得

前言

这篇文章主要是为了纪录一些自己对于setState的认识的不断深入的过程。我觉得这过程对我自己来说很有价值,不光是层层递进的了解一个API的执行机制,更对react总体的设计有了更为深入的认识。

第一阶段 初识setState

使用过React的应该都知道,在React中,一个组件中要读取当前状态需要访问this.state,但是更新状态却需要使用this.setState,不是直接在this.state上修改,就比如这样:

//读取状态
const count = this.state.count;

//更新状态
this.setState({count: count + 1});

//无意义的修改
this.state.count = count + 1;

其实这主要有几点考虑,首先this.state说到底只是一个对象,单纯的去修改一个对象的值是毫无意义的,在React中只有去驱动UI的更新才会有意义,因此虽然我们可以尝试直接改变this.state,但并没有驱动UI的重新渲染,因此这种操作也就毫无意义。也正是由于这个原因,我们就需要使用this.setState来驱动组件的更新过程。

然后在我刚学习React时,我就看见了这段很经典的代码:

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

作为一名JSer,我看完就毫不犹豫的想到,这特么不就是count的值加3么,但转眼看了下面的答案,光速打脸,实际的结果是state只增加了1。然后我就不由想到当时没怎么看懂的React文档中的一些话:状态更新可能是异步的,状态更新合并。恩,没毛病,因为异步且会合并,因此这三条语句合并为一条语句了,所以就只执行一次。然后就扭头溜了,并没有去思考一些深层次的问题。

第二阶段 setState理解的进阶

但是随着对React的理解的逐步加深,我开始对setState有了更加深的理解:

首先我意识到this.setState会通过引发一次组件的更新过程来引发重新绘制。也就是说setState的调用会引起React的更新生命周期的四个函数的依次调用:

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

我们都知道,在React生命周期函数里,以render函数为界,无论是挂载过程和更新过程,在render之前的几个生命周期函数,this.state和Props都是不会发生更新的,直到render函数执行完毕后,this.state才会得到更新。(有一个例外:当shouldComponentUpdate函数返回false,这时候更新过程就被中断了,render函数也不会被调用了,这时候React不会放弃掉对this.state的更新的,所以虽然不调用render,依然会更新this.state。)

React的官方文档有提到过这么一句话:

状态更新会合并(也就是说多次setstate函数调用产生的效果会合并)。

起初我对这句话理解并不是很深刻,但按照官方文档的代码示例写了这么一段代码:

function updateName() {
  this.setState({Age: '22'})
  this.setState({Name: 'srtian'})
}

果然执行结果与以下代码是等价的

function updateName() {
  this.setState({Age: '22', Name: 'srtian})
}

于是我将其理解为一个队列,每个this.setState()都会被合并起来,排成一排,到最后一次解决。但对其设计的原因并不理解,只知道这样有利于性能(也是在文档上看到的)。

直到理解上面React生命周期函数的原理后,我才理解了setState关于这个设计的意图。

前面我们提到过,每一次使用setState都会调用一次更新的生命周期,如果每一次this.serState()都调用一次上面那四个生命周期函数,虽然以上四个函数都是纯函数,性能浪费上还好,但render函数会将结果拿去做Virtual DOM比较和更新DOM树,这个就比较费时间。因此,将多个this.setSate进行合并,render函数就能够将合并后的this.setState()的结果一次性的与Virtual DOM比较然后更新DOM树,这样就能够用有效的提升性能。

除此之外,我还认为setState的设计十分巧妙,一般来说只在render函数后才会进行更新this.state。这其实也避免了React16的Fiber可能会产生的一个问题:由于Fiber下的组件更新是可以中断,也就是说在一个组件的更新过程中,可能更新到一半的时候就由于其他原因而中断更新,回去做更重要的事情了,在做完更重要的事情后,再回来更新这个组件,这会导致前面的那些生命周期函数可能会执行多次。因此如果在render之前this.setState()就改变状态的话,很有可能就会导致组件状态的多次更新,从而导致组件状态的混乱。

第三阶段 从源码理解setstate

这是React15.6版本,由于React16变动较大,setState的调用栈发生变动,因此仅供参考。

经历了上面那个阶段,我算是对setState有那么一些理解了,但还是不能理解很多东西比如:this.setState()的是怎么合并的?setState()到底是怎样一种骚操作?...等等。然后我又看见了这段经典的代码:

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

恩!按照我多年经验,这波操作我看不懂!

image

于是硬着头皮打开了React源码,开始一波瞎分析:
首先就是setState了,可以看出它接受两个参数partialState和callback,其中partialState顾名思义就是部分state,起这个名字也能就是想表达它的state没有改变(瞎猜的。。。)。以下是省略了一部分的代码,只看核心部分。

ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
 enqueueSetState: function(publicInstance, partialState) {
    if (__DEV__) {
      ReactInstrumentation.debugTool.onSetState();
      warning(
        partialState != null,
        'setState(...): You passed an undefined or null state object; ' +
          'instead, use forceUpdate().',
      );
    }

    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState',
    );

    if (!internalInstance) {
      return;
    }

    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  }
// 通过enqueueUpdate执行state更新
function enqueueUpdate(component) {
  ensureInjected();
  // batchingStrategy是批量更新策略,isBatchingUpdates表示是否处于批量更新过程
  // 最开始默认值为false
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  dirtyComponents.push(component);

  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
// 对_pendingElement, _pendingStateQueue, _pendingForceUpdate进行判断,
// _pendingStateQueue由于会对state进行修改,所以不为空,
// 然后会调用updateComponent方法
performUpdateIfNecessary: function(transaction) {
    if (this._pendingElement != null) {
      ReactReconciler.receiveComponent(
        this,
        this._pendingElement,
        transaction,
        this._context,
      );
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(
        transaction,
        this._currentElement,
        this._currentElement,
        this._context,
        this._context,
      );
    } else {
      this._updateBatchNumber = null;
    }
  },

其中这段代码需要额外注意:

  // batchingStrategy是批量更新策略,isBatchingUpdates表示是否处于批量更新过程
  // 最开始默认值为false
if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }

上面这段代码的意思就是如果是处于批量更新模式,也就是isBatchingUpdates为true时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中。
如果不处于批量更新模式,则对所有队列中的更新执行batchedUpdates方法。

然后可以找到了这个batchedUpdates:

var ReactDefaultBatchingStrategy = {
  // 也就是上面提到的默认为false
  isBatchingUpdates: false,
  // 这个方法只有在isBatchingUpdates: false时才会调用
  // 但一般来说,处于react大事务中时,会在render中的_renderNewRootComponent中将其设置为true。
  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) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },

看到这我总算理解了,当我们调用setState时,最终会通过enqueueUpdate执行state更新,就像上面那样有两种更新的模式,一种是批量更新模式,将组建保存在dirtyComponents;另一种非批量模式,将会遍历dirtyComponents,对每一个dirtyComponents调用updateComponent方法。就像这张图:

流程图

至于批量与非批量模式,会通过ReactDefaultBatchingStrategy中的isBatchingUpdates属性来进行判断。在非批量模式下,会立即应用新的state;而在批量模式下,需要更新state的组件会被push 到dirtyComponents,再执行更新。

所以我们再看前面的那坨代码:

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

就不难看出它的答案是 0, 0, 2, 3。

总结起来就是这样:

  • this.setState首先会把state推入pendingState队列中
  • 然后将组件标记为dirty
  • React中有事务的概念,最常见的就是更新事务,如果不在事务中,则会开启一次新的更新事务,更新事务执行的操作就是把组件标记为dirty。
  • 判断是否处于batch update
  • 是的话,保存组建于dirtyComponent中,在事务结束的时候才会通过 ReactUpdates.flushBatchedUpdates 方法将所有的临时 state merge 并计算出最新的 props 及 state,然后将其批量执行,最后再关闭结束事务。
  • 不是的话,直接开启一次新的更新事务,在标记为dirty之后,直接开始更新组件。因此当setState执行完毕后,组件就更新完毕了,所以会造成定时器同步更新的情况。

另外还有就是updateComponent方法,这也很重要:

{   // 会检测组件中的state和props是否发生变化,有变化才会进行更新; 
    // 如果shouldUpdateComponent函数中返回false则不会执行组件的更新
    updateComponent: function (transaction,
                               prevParentElement,
                               nextParentElement,
                               prevUnmaskedContext,
                               nextUnmaskedContext,) {
        var inst = this._instance;
        var nextState = this._processPendingState(nextProps, nextContext);
        var shouldUpdate = true;

        if (!this._pendingForceUpdate) {
            if (inst.shouldComponentUpdate) {
                if (__DEV__) {
                    shouldUpdate = measureLifeCyclePerf(
                            () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
                            this._debugID,
                            'shouldComponentUpdate',
                    );
                } else {
                    shouldUpdate = inst.shouldComponentUpdate(
                            nextProps,
                            nextState,
                            nextContext,
                    );
                }
            } else {
                if (this._compositeType === CompositeTypes.PureClass) {
                    shouldUpdate =
                            !shallowEqual(prevProps, nextProps) ||
                            !shallowEqual(inst.state, nextState);
                }
            }
        }
        
    },
// 该方法会合并需要更新的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 = 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;
    }
};

发现它会调用shouldComponentUpdate和componentWillUpdate方法,看到这不由理解了一个定律:不要在shouldComponentUpdate和componentWillUpdate中调用setState。如果在这两个生命周期里调用setState,会造成造成循环调用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容