React setState 简单整理总结

写业务代码的时候 需要经常用到setState, 前几天review代码的时候, 又想了一下这个API, 发现对它的了解不是很清楚, 仅仅是 setState 是异步的, 周六在家参考了一些资料,简单整理了下,写的比较简单, 通篇阅读大概耗时 5min, 在这简单分享一下, 希望对大家有所帮助 ;)。

先看一个例子

假如有这样一个点击执行累加场景:

// …
this.state = {
  count: 0,
}

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

handleIncrement = () => {
 this.incrementCount();
 this.incrementCount();
 this.incrementCount();
}
// ..

每一次点击, 累加三次,看一下输入:

并没有达到预期的效果,纠正也很简单:

incrementCount() {
  this.setState((prevState) => {
    return {count: prevState.count + 1}
  });
}

再看输出:

setState 的时候, 一个传入了object, 一个传入了更新函数。

区别在于: 传入一个更新函数,就可以访问当前状态值。 setState调用是 批量处理的,因此可以让更新建立在彼此之上,避免冲突。

那问题来了, 为什么前一种方式就不行呢? 带着这个疑问,继续往下看。

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树,这个就比较费时间。

目前React会将setState的效果放在队列中,积攒着一次引发更新过程。
为的就是把Virtual DOM和DOM树操作降到最小,用于提高性能。

查阅一些资料后发现,某些操作还是可以同步更新this.state的。

setState 什么时候会执行同步更新?
先直接说结论吧:
在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。

所谓“除此之外”,指的是绕过React通过
addEventListener
直接添加的事件处理函数,还有通过
setTimeout || setInterval

产生的异步调用。

简单一点说, 就是经过React 处理的事件是不会同步更新this.state的. 通过 addEventListener || setTimeout/setInterval 的方式处理的则会同步更新。
具体可以参考 jsBin 的这个例子。

结果就很清晰了:

点击Increment ,执行onClick ,输出0;
而通过addEventListener , 和 setTimeout 方式处理的, 第一次 直接输出了1;

理论大概是这样的,盗用一张图:

image.png

在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;

<ins data-ad-format="auto" class="adsbygoogle adsbygoogle-noablate" data-ad-client="ca-pub-6330872677300335" data-adsbygoogle-status="done" style="box-sizing: border-box; display: block; margin: auto; background-color: transparent;"><ins id="aswift_6_expand" style="box-sizing: border-box; display: inline-table; border: none; height: 0px; margin: 0px; padding: 0px; position: relative; visibility: visible; width: 825px; background-color: transparent;"><ins id="aswift_6_anchor" style="box-sizing: border-box; display: block; border: none; height: 0px; margin: 0px; padding: 0px; position: relative; visibility: visible; width: 825px; background-color: transparent; overflow: hidden; opacity: 0;"><iframe width="825" height="200" frameborder="0" marginwidth="0" marginheight="0" vspace="0" hspace="0" allowtransparency="true" scrolling="no" allowfullscreen="true" onload="var i=this.id,s=window.google_iframe_oncopy,H=s&&s.handlers,h=H&&H[i],w=this.contentWindow,d;try{d=w.document}catch(e){}if(h&&d&&(!d.body||!d.body.firstChild)){if(h.call){setTimeout(h,0)}else if(h.match){try{h=s.upd(h,i)}catch(e){}w.location.replace(h)}}" id="aswift_6" name="aswift_6" style="box-sizing: border-box; left: 0px; position: absolute; top: 0px; border: 0px; width: 825px; height: 200px;"></iframe></ins></ins></ins>

// 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。

故 getInitialState,componentWillMount, render,componentWillUpdate 中 setState 都不会引起 updateComponent。

但在componentDidMount 和 componentDidUpdate中则会。

事务
事务通过wrapper进行封装。

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

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

ReactDefaultBatchingStrategy.isBatchingUpdates = false;

}};

transcation被包装在wrapper中,比如:
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

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

下面分析transaction.perform(callback, args…)

perform: function (method, scope, a, b, c, d, e, f) {

var errorThrown;
var ret;
try {
  this._isInTransaction = true;
  errorThrown = true;
  // 先运行所有wrapper中的initialize方法
  this.initializeAll(0);

  // 再执行perform方法传入的callback
  ret = method.call(scope, a, b, c, d, e, f);
  errorThrown = false;
} finally {
  try {
    if (errorThrown) {
      // 最后运行wrapper中的close方法
      try {
        this.closeAll(0);
      } catch (err) {}
    } else {
      // 最后运行wrapper中的close方法
      this.closeAll(0);
    }
  } finally {
    this._isInTransaction = false;
  }
}
return ret;

},

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会在一个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。

从而完成组件更新的整套流程。

整体流程回顾:
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。

看完理论, 我们再用一个例子巩固下:
再看一个例子:

class Example extends React.Component {
constructor() {
super();
this.state = {


val: 0


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

setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log('第 3 次 log:', this.state.val); 
this.setState({val: this.state.val + 1});
console.log('第 4 次 log:', this.state.val); 
}, 0);
}
render() {
return null;
}
};

前两次在isBatchingUpdates 中,没有更新state, 输出两个0。
后面两次会同步更新, 分别输出2, 3;

很显然,我们可以将4次setState简单规成两类:
componentDidMount是一类
setTimeOut中的又是一类,因为这两次在不同的调用栈中执行。

我们先看看在componentDidMount中setState的调用栈:

再看看在setTimeOut中的调用栈:

我们重点看看在componentDidMount中的sw3e调用栈 :
发现了batchedUpdates方法。

原来在setState调用之前,就已经处于batchedUpdates执行的事务之中了。

那batchedUpdates方法是谁调用的呢?我们再往上追溯一层,原来是ReactMount.js中的_renderNewRootComponent方法。

也就是说,整个将React组件渲染到DOM的过程就处于一个大的事务中了。

接下来就很容易理解了: 因为在componentDidMount中调用setState时,batchingStrategy的isBatchingUpdates已经被设置为true,所以两次setState的结果并没有立即生效,而是被放进了dirtyComponents中。

这也解释了两次打印this.state.val都是0的原因,因为新的state还没被应用到组件中。

再看setTimeOut中的两次setState,因为没有前置的batchedUpdate调用,所以batchingStrategy的isBatchingUpdates标志位是false,也就导致了新的state马上生效,没有走到dirtyComponents分支。

也就是说,setTimeOut中的第一次执行,setState时,this.state.val为1;
而setState完成后打印时this.state.val变成了2。

第二次的setState同理。

通过上面的例子,我们就知道setState 是可以同步更新的,但是还是尽量避免直接使用, 仅作了解就可以了。

如果你非要玩一些骚操作,写出这样的代码去直接去操作this.state:
this.state.count = this.state.count + 1;
this.state.count = this.state.count + 1;
this.state.count = this.state.count + 1;
this.setState();

我只能说, 大胸弟, 你很骚。吾有旧友叼似汝,而今坟草丈许高。

结语
最后简单重复下结论吧:

  • 不要直接去操作this.state, 这样会造成不必要的性能问题和隐患。
  • 由React引发的事件处理,调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。

我对这一套理论也不是特别熟悉, 如有纰漏, 欢迎指正 :)

扩展阅读
https://reactjs.org/docs/faq-...
https://reactjs.org/docs/reac...
https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...

https://medium.com/@wisecobbl...

https://zhuanlan.zhihu.com/p/...

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