由实际问题探究setState的执行机制

一.几个开发中经常会遇到的问题

以下几个问题是我们在实际开发中经常会遇到的场景,下面用几个简单的示例代码来还原一下。

1.setState是同步还是异步的,为什么有的时候不能立即拿到更新结果而有的时候可以?

1.1 钩子函数和React合成事件中的setState

现在有两个组件

 componentDidMount() {

   console.log('parent componentDidMount');

 }

 render() {

   return (

     <div>

       <SetState2></SetState2>

       <SetState></SetState>

     </div>

   );

 }

组件内部放入同样的代码,并在Setstate1中的componentDidMount中放入一段同步延时代码,打印延时时间:

 componentWillUpdate() {

   console.log('componentWillUpdate');

 }

 componentDidUpdate() {

   console.log('componentDidUpdate');

 }

 componentDidMount() {

   console.log('SetState调用setState');

   this.setState({

     index: this.state.index + 1

   })

   console.log('state', this.state.index);

   console.log('SetState调用setState');

   this.setState({

     index: this.state.index + 1

   })

   console.log('state', this.state.index);

 }

下面是执行结果:

说明:

1.调用setState不会立即更新

2.所有组件使用的是同一套更新机制,当所有组件didmount后,父组件didmount,然后执行更新

3.更新时会把每个组件的更新合并,每个组件只会触发一次更新的生命周期。

1.2 异步函数和原生事件中的setstate?

在setTimeout中调用setState(例子和在浏览器原生事件以及接口回调中执行效果相同)

 componentDidMount() {

   setTimeout(() => {

     console.log('调用setState');

     this.setState({

       index: this.state.index + 1

     })

     console.log('state', this.state.index);

     console.log('调用setState');

     this.setState({

       index: this.state.index + 1

     })

     console.log('state', this.state.index);

   }, 0);

 }

执行结果:

说明:

1.在父组件didmount后执行

2.调用setState同步更新

2.为什么有时连续两次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不会被合并

二.setState执行过程

由于源码比较复杂,就不贴在这里了,有兴趣的可以去github上clone一份然后按照下面的流程图去走一遍。

1.流程图

partialState:setState传入的第一个参数,对象或函数

_pendingStateQueue:当前组件等待执行更新的state队列

isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用

dirtyComponent:当前所有处于待更新状态的组件队列

transcation:react的事务机制,在被事务调用的方法外包装n个waper对象,并一次执行:waper.init、被调用方法、waper.close

FLUSH_BATCHED_UPDATES:用于执行更新的waper,只有一个close方法

2.执行过程

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

1.将setState传入的partialState参数存储在当前组件实例的state暂存队列中。

2.判断当前React是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。

3.如果未处于批量更新状态,将批量更新状态标识设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。

4.调用事务的waper方法,遍历待更新组件队列依次执行更新。

5.执行生命周期componentWillReceiveProps。

6.将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将队列置为空。

7.执行生命周期componentShouldUpdate,根据返回值判断是否要继续更新。

8.执行生命周期componentWillUpdate。

9.执行真正的更新,render。

10.执行生命周期componentDidUpdate。

三.总结

1.钩子函数和合成事件中:

在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。

按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。

当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState。

2.异步函数和原生事件中

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

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

3.partialState合并机制

我们看下流程中_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是前一次合并后的结果,所以计算结果是准确的。

4.componentDidMount调用setstate

在componentDidMount()中,你 可以立即调用setState()。它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。谨慎使用这一模式,因为它常导致性能问题。在大多数情况下,你可以 在constructor()中使用赋值初始状态来代替。然而,有些情况下必须这样,比如像模态框和工具提示框。这时,你需要先测量这些DOM节点,才能渲染依赖尺寸或者位置的某些东西。

以上是官方文档的说明,不推荐直接在componentDidMount直接调用setState,由上面的分析:componentDidMount本身处于一次更新中,我们又调用了一次setState,就会在未来再进行一次render,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。

当然在componentDidMount我们可以调用接口,再回调中去修改state,这是正确的做法。

当state初始值依赖dom属性时,在componentDidMount中setState是无法避免的。

5.componentWillUpdatecomponentDidUpdate

这两个生命周期中不能调用setState。

由上面的流程图很容易发现,在它们里面调用setState会造成死循环,导致程序崩溃。

6.推荐使用方式

在调用setState时使用函数传递state值,在回调函数中获取最新更新后的state。

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

推荐阅读更多精彩内容

  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,428评论 1 33
  • 起步 安装官方脚手架: npm install -g create-react-app 创建项目: create-...
    Twoold阅读 1,203评论 0 0
  • 在react中,通过管理状态来实现对组件的管理,通过this.state()来访问state,通过this.set...
    青艹止阅读 1,112评论 0 1
  • 40、React 什么是React?React 是一个用于构建用户界面的框架(采用的是MVC模式):集中处理VIE...
    萌妹撒阅读 1,005评论 0 1
  • React一些特点 专注于视图层 业务框架(也可以理解为model层)可以根据业务场景自行选择。 Virtual ...
    vam笙阅读 168评论 0 0