多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
React 是通过管理状态来实现对组件的管理,即使用 this.state 获取 state,通过 this.setState() 来更新 state,当使用 this.setState() 时,React 会调用 render 方法来重新渲染 UI。 首先看一个例子: ~~~js 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,你做对了吗?** ![](https://img.kancloud.cn/cb/ac/cbac1915842347982e0411342f13808a_279x216.png) ### 一、setState 异步更新 setState 通过一个**队列机制**来实现 state 更新,当执行 setState() 时,会将需要更新的 state**浅合并**后放入 状态队列,而不会立即更新 state,队列机制可以高效的**批量更新**state。而如果不通过setState,直接修改this.state 的值,则不会放入状态队列,当下一次调用 setState 对状态队列进行合并时,之前对 this.state 的修改将会被忽略,造成无法预知的错误。 React通过状态队列机制实现了 setState 的异步更新,避免重复的更新 state。 ~~~js setState(nextState, callback) ~~~ 在 setState 官方文档中介绍:**将 nextState 浅合并到当前 state。这是在事件处理函数和服务器请求回调函数中触发 UI 更新的主要方法。不保证`setState`调用会同步执行,考虑到性能问题,可能会对多次调用作批处理。** 举个例子: ~~~js // 假设 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 ~~~ 本质上等同于: ~~~js // 假设 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。 ~~~js // 正确用法 this.setState((prevState, props) => ({ count: prevState.count + props.increment })) ~~~ 这种函数式 setState() 工作机制类似: ~~~js [ {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 应该是[纯函数](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976)。 > 注意:企图直接修改`prevState`通常都是初学者困惑的根源。 相关源码: ~~~js // 将新的 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`方法,因此造成循环调用,使得浏览器内存占满后崩溃。 ![](https://img.kancloud.cn/b9/b9/b9b91e11d5fde2d352d5e4f273fdd649_1414x710.png) **图 2-1 循环调用** setState 源码: ~~~js // 更新 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 的喃? ![](https://img.kancloud.cn/00/d8/00d834d06084bd2789fe249dadff4b5d_1830x1246.png) **图3-1 setState 简化调用栈** 上面这个流程图是一个简化的 setState 调用栈,注意其中核心的状态判断,在[源码(ReactUpdates.js)](http://link.zhihu.com/?target=https%3A//github.com/facebook/react/blob/35962a00084382b49d1f9e3bd36612925f360e5b/src/renderers/shared/reconciler/ReactUpdates.js%23L199)中 ~~~ 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 的[源码](http://link.zhihu.com/?target=https%3A//github.com/facebook/react/blob/6d5fe44c8602f666a043a4117ccc3bdb29b86e78/src/shared/utils/Transaction.js)中有一幅特别的 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](http://link.zhihu.com/?target=https%3A//github.com/facebook/react/blob/401e6f10587b09d4e725763984957cf309dfdc30/src/shared/utils/Transaction.js)实现。 说了这么多 Transaction,它到底是怎么导致上文所述 setState 的各种不同表现的呢? ### 五、解密 setState 那么 Transaction 跟 setState 的不同表现有什么关系呢?首先我们把 4 次 setState 简单归类,前两次属于一类,因为他们在同一次调用栈中执行;setTimeout 中的两次 setState 属于另一类,原因同上。让我们分别看看这两类 setState 的调用栈: ![](https://img.kancloud.cn/e2/0c/e20ce43158e3154575ce464f010c3a3b_446x341.png) **图 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 的情况: ~~~js 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中,**如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state**。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。 \*\*原因:\*\*在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,**有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state**。 对于异步渲染,我们应在`getSnapshotBeforeUpdate`中读取`state`、`props`, 而不是`componentWillUpdate`。但调用`forceUpdate()`强制 render 时,会导致组件跳过`shouldComponentUpdate()`,直接调用`render()`。 ## 摘自 [深入 setState 机制]([https://github.com/sisterAn/blog/issues/26](https://github.com/sisterAn/blog/issues/26))