React setState机制

转自:https://blog.csdn.net/lunahaijiao/article/details/86995969

React 是通过管理状态来实现对组件的管理,即使用 this.state 获取 state,通过 this.setState() 来更新 state,当使用 this.setState() 时,React 会调用 render 方法来重新渲染 UI。

首先看一个例子:

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,你做对了吗?

一、setState 异步更新

setState 通过一个队列机制来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并后放入 状态队列,而不会立即更新 state,队列机制可以高效的批量更新 state。而如果不通过setState,直接修改this.state 的值,则不会放入状态队列,当下一次调用 setState 对状态队列进行合并时,之前对 this.state 的修改将会被忽略,造成无法预知的错误。

React通过状态队列机制实现了 setState 的异步更新,避免重复的更新 state。

setState(nextState, callback)

1

在 setState 官方文档中介绍:将 nextState 浅合并到当前 state。这是在事件处理函数和服务器请求回调函数中触发 UI 更新的主要方法。不保证 setState 调用会同步执行,考虑到性能问题,可能会对多次调用作批处理。

举个例子:

// 假设 state.count === 0

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

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

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

// state.count === 1, 而不是 3

本质上等同于:

// 假设 state.count === 0

Object.assign(state,

              {count: state.count + 1},

              {count: state.count + 1},

              {count: state.count + 1}

            )

// {count: 1}

但是如何解决这个问题喃,在文档中有提到:

也可以传递一个签名为 function(state, props) => newState 的函数作为参数。这会将一个原子性的更新操作加入更新队列,在设置任何值之前,此操作会查询前一刻的 state 和 props。...setState() 并不会立即改变 this.state ,而是会创建一个待执行的变动。调用此方法后访问 this.state 有可能会得到当前已存在的 state(译注:指 state 尚未来得及改变)。

即使用 setState() 的第二种形式:以一个函数而不是对象作为参数,此函数的第一个参数是前一刻的state,第二个参数是 state 更新执行瞬间的 props。

// 正确用法

this.setState((prevState, props) => ({

    count: prevState.count + props.increment

}))

这种函数式 setState() 工作机制类似:

[

    {increment: 1},

    {increment: 1},

    {increment: 1}

].reduce((prevState, props) => ({

    count: prevState.count + props.increment

}), {count: 0})

// {count: 3}

关键点在于更新函数(updater function):

(prevState, props) => ({

  count: prevState.count + props.increment

})

这基本上就是个 reducer,其中 prevState 类似于一个累加器(accumulator),而 props 则像是新的数据源。类似于 Redux 中的 reducers,你可以使用任何标准的 reduce 工具库对该函数进行 reduce(包括 Array.prototype.reduce())。同样类似于 Redux,reducer 应该是 纯函数 。

注意:企图直接修改 prevState 通常都是初学者困惑的根源。

相关源码:

// 将新的 state 合并到状态队列

var nextState = this._processPendingState(nextProps, nextContext)

// 根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件

var shouldUpdate = this._pendingForceUpdate ||

    !inst.shouldComponentUpdate ||

    inst.shouldComponentUpdate(nextProps, nextState, nextContext)

二、setState 循环调用风险

当调用 setState 时,实际上是会执行 enqueueSetState 方法,并会对 partialState 及 _pendingStateQueue 队列进行合并操作,最终通过 enqueueUpdate 执行 state 更新。

而 performUpdateIfNecessary 获取 _pendingElement、_pendingStateQueue、_pendingForceUpdate,并调用 reaciveComponent 和 updateComponent 来进行组件更新。

**但,如果在 shouldComponentUpdate 或 componentWillUpdate 方法里调用 this.setState 方法,就会造成崩溃。**这是因为在 shouldComponentUpdate 或 componentWillUpdate 方法里调用 this.setState 时,this._pendingStateQueue!=null,则 performUpdateIfNecessary 方法就会调用 updateComponent 方法进行组件更新,而 updateComponent 方法又会调用 shouldComponentUpdate和componentWillUpdate 方法,因此造成循环调用,使得浏览器内存占满后崩溃。

图 2-1 循环调用

setState 源码:

// 更新 state

ReactComponent.prototype.setState = function(partialState, callback) {

    this.updater.enqueueSetState(this, partialState)

    if (callback) {

        this.updater.enqueueCallback(this, callback, 'setState')

    }

}

enqueueSetState: function(publicInstance, partialState) {

    var internalInstance = getInternalInstanceReadyForUpdate(

        publicInstance,

        'setState'

    )

    if (!internalInstance) {

        return

    }


    // 更新队列合并操作

    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue=[])

    queue.push(partialState)

    enqueueUpdate(internalInstance)

}

// 如果存在 _pendingElement、_pendingStateQueue、_pendingForceUpdate,则更新组件

performUpdateIfNecessary: function(transaction) {

    if (this._pendingElement != null) {

        ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context)

    }


    if (this._pendingStateQueue != null || this._pendingForceUpdate) {

        this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context)

    }

}

三、setState 调用栈

既然 setState 是通过 enqueueUpdate 来执行 state 更新的,那 enqueueUpdate 是如何实现更新 state 的喃?

图3-1 setState 简化调用栈

上面这个流程图是一个简化的 setState 调用栈,注意其中核心的状态判断,在源码(ReactUpdates.js)中

function enqueueUpdate(component) {

  // ...

  if (!batchingStrategy.isBatchingUpdates) {

    batchingStrategy.batchedUpdates(enqueueUpdate, component);

    return;

  }

  dirtyComponents.push(component);

}

若 isBatchingUpdates 为 false 时,所有队列中更新执行 batchUpdate,否则,把当前组件(即调用了 setState 的组件)放入 dirtyComponents 数组中。先不管这个 batchingStrategy,看到这里大家应该已经大概猜出来了,文章一开始的例子中 4 次 setState 调用表现之所以不同,这里逻辑判断起了关键作用。

那么 batchingStrategy 究竟是何方神圣呢?其实它只是一个简单的对象,定义了一个 isBatchingUpdates 的布尔值,和一个 batchedUpdates 方法。下面是一段简化的定义代码:

var batchingStrategy = {

  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {

    // ...

    batchingStrategy.isBatchingUpdates = true;


    transaction.perform(callback, null, a, b, c, d, e);

  }

};

注意 batchingStrategy 中的 batchedUpdates 方法中,有一个 transaction.perform 调用。这就引出了本文要介绍的核心概念 —— Transaction(事务)。

四、初识事物

在 Transaction 的源码中有一幅特别的 ASCII 图,形象的解释了 Transaction 的作用。

/*

* <pre>

*                      wrappers (injected at creation time)

*                                      +        +

*                                      |        |

*                    +-----------------|--------|--------------+

*                    |                v        |              |

*                    |      +---------------+  |              |

*                    |  +--|    wrapper1  |---|----+        |

*                    |  |  +---------------+  v    |        |

*                    |  |          +-------------+  |        |

*                    |  |    +----|  wrapper2  |--------+  |

*                    |  |    |    +-------------+  |    |  |

*                    |  |    |                    |    |  |

*                    |  v    v                    v    v  | wrapper

*                    | +---+ +---+  +---------+  +---+ +---+ | invariants

* perform(anyMethod) | |  | |  |  |        |  |  | |  | | maintained

* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->

*                    | |  | |  |  |        |  |  | |  | |

*                    | |  | |  |  |        |  |  | |  | |

*                    | |  | |  |  |        |  |  | |  | |

*                    | +---+ +---+  +---------+  +---+ +---+ |

*                    |  initialize                    close    |

*                    +-----------------------------------------+

* </pre>

*/

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

具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。

下面是一个简单使用 Transaction 的例子

var Transaction = require('./Transaction');

// 我们自己定义的 Transaction

var MyTransaction = function() {

  // do sth.

};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {

  getTransactionWrappers: function() {

    return [{

      initialize: function() {

        console.log('before method perform');

      },

      close: function() {

        console.log('after method perform');

      }

    }];

  };

});

var transaction = new MyTransaction();

var testMethod = function() {

  console.log('test');

}

transaction.perform(testMethod);

// before method perform

// test

// after method perform

当然在实际代码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。

说了这么多 Transaction,它到底是怎么导致上文所述 setState 的各种不同表现的呢?

五、解密 setState

那么 Transaction 跟 setState 的不同表现有什么关系呢?首先我们把 4 次 setState 简单归类,前两次属于一类,因为他们在同一次调用栈中执行;setTimeout 中的两次 setState 属于另一类,原因同上。让我们分别看看这两类 setState 的调用栈:

图 5-1 componentDidMount 里的 setState 调用栈

图 5-2 setTimeout 里的 setState 调用栈

很明显,在 componentDidMount 中直接调用的两次 setState,其调用栈更加复杂;而 setTimeout 中调用的两次 setState,调用栈则简单很多。让我们重点看看第一类 setState 的调用栈,有没有发现什么熟悉的身影?没错,就是batchedUpdates 方法,原来早在 setState 调用前,已经处于 batchedUpdates 执行的 transaction 中!

那这次 batchedUpdate 方法,又是谁调用的呢?让我们往前再追溯一层,原来是 ReactMount.js 中的**_renderNewRootComponent** 方法。也就是说,整个将 React 组件渲染到 DOM 中的过程就处于一个大的 Transaction 中。

六、回到题目

接下来的解释就顺理成章了,因为在 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 同理。

在上文介绍 Transaction 时也提到了其在 React 源码中的多处应用,想必调试过 React 源码的同学应该能经常见到它的身影,像 initialize、perform、close、closeAll、notifyAll 等方法出现在调用栈里时,都说明当前处于一个 Transaction 中。

既然事务那么有用,那我们可以用它吗?

答案是不能,但在 React 15.0 之前的版本中还是为开发者提供了 batchedUpdates 方法,它可以解决针对一开始例子中 setTimeout 里的两次 setState 导致 rendor 的情况:

import ReactDom, { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {

  this.setState(val: this.state.val + 1);

  this.setState(val: this.state.val + 1);

});

在 React 15.0 之后的版本已经将 batchedUpdates 彻底移除了,所以,不再建议使用。

本文是《深入React技术栈》解密setState读书笔记以及自己的一些补充理解

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

推荐阅读更多精彩内容

  • 写业务代码的时候 需要经常用到setState, 前几天review代码的时候, 又想了一下这个API, 发现对它...
    world_7735阅读 1,646评论 0 3
  • 概述: setState通过一个队列机制实现state更新。 当执行setState时,会将需要更新的state合...
    南慕瑶阅读 409评论 0 0
  • 40、React 什么是React?React 是一个用于构建用户界面的框架(采用的是MVC模式):集中处理VIE...
    萌妹撒阅读 1,001评论 0 1
  • 在react中,通过管理状态来实现对组件的管理,通过this.state()来访问state,通过this.set...
    青艹止阅读 1,100评论 0 1
  • 这一天 天风拂动你的发丝 圣水洗礼你的心情 你的面部表现出了出奇的静 你远离了浮躁纷扰 你学会了静静深思 你学会了...
    花香一路阅读 207评论 2 11