ReactJS setState() 究竟有何问题?

ReactJS setState详解

this.state和this.props的更新多是异步的,React可能会出于性能考虑,将多个setState的调用,合并到一次State的更新中。

this.state的值计算下一个状态。引用官网的一个代码示例:
// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});
若是必定要这么作,可使用另外一个以函数做为参数的setState方法,这个函数的第一个参数是前一个State,第二个参数是当前接收到的最新Props。以下所示:
// Correct
this.setState(function(prevState, props) {
  return {
    counter: prevState.counter + props.increment
  };
});
在调用setState以后,也不能当即使用this.state获取最新状态,由于这时的state极可能尚未被更新,要想保证获取到的state是最新的state,能够在componentDidUpdate中获取this.state。也可使用带用回调函数参数版本的setStatesetState(stateChange, [callback]),回调函数中的this.state会保证是最新的state。
复制代码
0x00
The SyntheticEventis pooled. This means that the SyntheticEventobject will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.在 React 事件处理中,事件对象被包装在一个 SyntheticEvent(合成事件)对象中。这些对象是被池化的(pooled),这意味着在事件处理程序会把这些对象重用于其余事件以提升性能。随之而来的问题就是,异步访问事件对象的属性是不可能的,由于事件的属性因为重用而被重置(nullified)。
0x01
下面代码存在问题:
function handleClick(event) {
  setTimeout(function () {
    console.log(event.target.name);
  }, 1000);
}

控制台会输出 null,由于每次事件回调完成后,SyntheticEvent 会被重置。
解决方式是把 event 赋值到一个内部变量上。
function handleClick(event) {
  let name = event.target.name;    // 内部变量保存 event.target.name 的值
  setTimeout(function () {
    console.log(name);
  }, 1000);
}

0x02
facebook 官方的实例:
function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // Won't work. this.state.clickEvent will only contain null values. this.setState({clickEvent: event}); // You can still export event properties. this.setState({eventType: event.type}); } 若是想异步访问事件属性,能够在事件上调用 event.persist(),这会从池中移除合成事件并容许对事件的引用被保留。 复制代码
0x00
React has a setState() problem: Asking newbies to use setState() is a recipe for headaches. Advanced users have secret cures.为了性能和其它缘由,setState 这个 API 很容易被误用。
setState 不会马上改变 React 组件中 state 的值
setState 经过引起一次组件的更新过程来引起从新绘制
屡次 setState 函数调用产生的效果会合并

0x01
问题
看以下代码:
// 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 
三次操做被合并为了一次。

0x02
解决方式
this.setState((prevState, props) => ({
  count: prevState.count + props.increment
}));

要修复它,请使用第二种形式的 setState() 来接受一个函数而不是一个对象。 该函数将接收先前的状态做为第一个参数,将须要更新的值做为第二个参数:
// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

上方代码使用了箭头函数,但它也适用于常规函数:
// Correct
this.setState(function(prevState, props) {
  return {
    counter: prevState.counter + props.increment
  };
});
复制代码
setState(nextState, callback)  
复制代码

这是UI更新最经常使用的方法,合并新的state到现有的state。javascript

常规方式

nextState能够为一个对象,包含0个或多个要更新的key。最简单的用法为:html

this.setState({  
  key1: value1, 
  key2: value2
});
复制代码

这种方式能应付大部分的应用场景,可是看看下面这种状况:前端

this.setState({  
  count: this.state.count + 1
});
this.setState({  
  count: this.state.count + 1
});
复制代码

最后获得的count倒是不可控的。由于setState不会当即改变this.state,而是挂起状态转换,调用setState方法后当即访问this.state可能获得的是旧的值。java

setState方法不会阻塞state更新完毕react

第二个setState可能还没等待第一次的state更新完毕就开始执行了,因此最后count可能只加了1。git

这时setState的第二个参数就派上用场了,第二个参数是state更新完毕的回调函数es6

this.setState({  
  count: this.state.count + 1
}, () => {
  this.setState({
    count: this.state.count + 1
  });
});
复制代码

不过看起来很怪,es6中可使用Promise更优雅的使用这个函数,封装一下setStategithub

function setStateAsync(nextState){  
  return new Promise(resolve => {
    this.setState(nextState, resolve);
  });
}
复制代码

上面的例子就能够这样写编程

async func() {  
  ...
  await this.setStateAsync({count: this.state.count + 1});
  await this.setStateAsync({count: this.state.count + 1});
}
复制代码

顺眼多了。redux

函数方式

nextState也能够是一个function,称为状态计算函数,结构为function(state, props) => newState。这个函数会将每次更新加入队列中,执行时经过当前的stateprops来获取新的state。那么上面的例子就能够这样写

this.setState((state, props) => {  
  return {count: state.count + 1};
});
this.setState((state, props) => {  
  return {count: state.count + 1};
});
复制代码

每次更新时都会提取出当前的state,进行运算获得新的state,就保证了数据的同步更新。

控制渲染

默认调用setState都会从新渲染视图,可是经过shouldComponentUpdate()函数返回false来避免从新渲染。

若是可变对象没法在shouldComponentUpdate()函数中实现条件渲染,则须要控制newStateprevState不一样时才调用setState来避免没必要要的从新渲染。



这个问题能够有两个答案:

  1. 没啥问题。(大部分状况下)其表现和设计指望同样,足以解决目标问题。
  2. 学习曲线问题。对新手而言,一些用原生 JS 和直接的 DOM 操做能够轻松实现的效果,用 React 和 setState 实现起来就会困难重重。

React 的设计初衷本是简化应用开发流程,可是:

  • 你却不能为所欲为地操做 DOM。
  • 你不能为所欲为地(于任什么时候间、依赖任意数据源)更新 state。
  • 在组件的生命周期中,你并不老是能在屏幕上直接观察到渲染后的 DOM 元素,这限制了 setState() 的使用时机和方式(由于你有些 state 可能尚未渲染到屏幕上)。

在这几种状况下,困惑都来源于 React 组件生命周期的限制性(这些限制是刻意设计的,是好的)。

从属 State(Dependent State)

更新 state 时,更新结果可能依赖于:

  • 当前 state
  • 同一批次中先前的更新操做
  • 当前已渲染的 DOM (例如:组件的坐标位置、可见性、CSS 计算值等等)

当存在这几种从属 state 的时候,若是你还想简单直接地更新 state,那 React 的表现行为会让你大吃一惊,而且是以一种使人憎恶又难以调试的方式。大多数状况下,你的代码根本没法工做:要么 state 不对,要么控制台有错误。

我之因此吐槽 setState(),是由于它的这种限制性在 API 文档中并无详细说明,关于应对这种限制性的各类通用模式也未能阐述清楚。这迫使初学者只能不断试错、Google 或者从其余社区成员那里寻求帮助,但实际上在文档中本该就有更好的新手指南。

当前关于 setState() 的文档开头以下:

setState(nextState, callback)复制代码

将 nextState 浅合并到当前 state。这是在事件处理函数和服务器请求回调函数中触发 UI 更新的主要方法。

在末尾确实也提到了其异步行为:

不保证 setState 调用会同步执行,考虑到性能问题,可能会对屡次调用做批处理。

这就是不少用户层(userland) bug 的根本缘由:

// 假设 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复制代码

本质上等同于:

Object.assign(state,
  {count: state.count + 1},
  {count: state.count + 1},
  {count: state.count + 1}
); // {count: 1}复制代码

这在文档中并未显式说明(在另一份特殊指南中提到了)。

文档还提到了另一种函数式的 setState() 语法:

也能够传递一个签名为 function(state, props) => newState 的函数做为参数。这会将一个原子性的更新操做加入更新队列,在设置任何值以前,此操做会查询前一刻的 state 和 props。

...

setState() 并不会当即改变 this.state ,而是会建立一个待执行的变更。调用此方法后访问 this.state 有可能会获得当前已存在的 state(译注:指 state 还没有来得及改变)。

API 文档虽提供了些许线索,但未能以一种清晰明了的方式阐明初学者常常遇到的怪异表现。开发模式下,尽管 React 的错误信息以有效、准确著称,但当 setState() 的同步问题出现 bug 的时候控制台却没有任何警告。

Jikku Jose

Pier Bover

StackOverflow 上有关 setState() 的问题大都要归结于组件的生命周期问题。毫无疑问,React 很是流行,所以那些问题都被,也有着各类参差不齐的回答。

那么,初学者究竟该如何掌握 setState() 呢?

在 React 的文档中还有一份名为 “ 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}复制代码

不明白 reduce 的工做机制? 参见 “Composing Software”“Reduce” 教程。

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

(prevState, props) => ({
  count: prevState.count + props.increment
})复制代码

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

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

API 文档中并未说起更新函数的这些特性和要求,因此,即便少数幸运的初学者碰巧了解到函数式 setState() 能够实现一些对象字面量形式没法实现的功能,最终依然可能困惑不解。

仅仅是新手才有的问题吗?

直到如今,在处理表单或是 DOM 元素坐标位置的时候,我仍是会时不时得掉到坑里去。当你使用 setState() 的时候,你必须直接面对组件生命周期的相关问题;但当你使用容器组件或是经过 props 来存储和传递 state 的时候,React 则会替你处理同步问题。

不管你有经验与否 ,处理共享的可变 state 和 state 锁(state locks)都是很棘手的。经验丰富之人只不过是能更加快速地定位问题,而后找出一个巧妙的变通方案罢了。

由于初学者从未遇到过这种问题,更不知规避方案,因此是掉坑里摔得最惨的。

当问题发生时,你固然能够选择和 React 斗个你死我活;不过,你也能够选择让 React 顺其天然的工做。这就是我说即便是对初学者而言,Redux 有时 都比 setState 更简单的缘由。

在并发系统中,state 更新一般按其中一种方式进行:

  • 当其余程序(或代码)正在访问 state 时,禁止 state 的更新(例如 setState())(译注:即常见的锁机制)
  • 引入不可变性来消除共享的可变 state,从而实现对 state 的无限制访问,而且能够在任什么时候间建立新 state(例如 Redux)

在我看来(在向不少学生教授过这两种方法以后),相比于第二种方法,第一种方法更加容易致使错误,也更加容易使人困惑。当 state 更新被简单地阻塞时(在 setState 的例子中,也能够叫批处理化或延迟执行),解决问题的正确方法并不十分清晰明了。

当遇到 setState() 的同步问题时,个人直觉反应实际上是很简单的:将 state 的管理上移到 Redux(或 MobX) 或容器组件中。基于多方面缘由 ,我本身使用同时也推荐他人使用 Redux,但很显然,这并非一条放之四海而皆准的建议。

Redux 自有其陡峭的学习曲线,但它规避了共享的可变 state 以及 state 更新同步等复杂问题。所以我发现,一旦我教会了学生如何避免可变性,接下来基本就一路顺风了。

对于没有任何函数式编程经验的新手而言,学习 Redux 遇到的问题可能会比学习 setState()遇到的更多 —— 可是,Redux 至少有不少其做者亲自讲授的免费 教程

React 应当向 Redux 学习:有关 React 编程模式和 setState() 踩坑的视频教程定能让 React 主页锦上添花。

在渲染以前决定 State

将 state 管理移到容器组件(或 Redux)中能促使你从另外一个角度思考组件 state 问题,由于这种状况下,在组件渲染以前,其 state 必须是既定的(由于你必须将其做为 props 传下去)。

重要的事情说三遍:

渲染以前,决定 state!

渲染以前,决定 state!

渲染以前,决定 state!

说完三篇以后就能够获得一个显然的推论:在 render() 函数中调用 setState() 是反模式的。

render 函数中计算从属 state 是 OK 的(好比说, state 中有 firstNamelastName,据此你计算出 fullName,在 render 函数中这样作彻底是 OK 的),但我仍是倾向于在容器组件中计算出从属 state ,而后经过 props 将其传递给展现组件(presentation components)。

setState() 该怎么治?

我倾向于废弃掉对象字面量形式的 setState(),我知道这(表面上看)更加易于理解也更加方便(译者:“这”指对象字面量形式的 setState()),但它也是坑之所在啊。用脚指头都能猜到,确定有人这样写:

state.count; // 0
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});复制代码

而后天真就地觉得 {count: 3}。批量化处理后对象的同名 props 被合并掉的状况几乎不多是用户所指望的行为,反正我是没见过这种例子。要是真存在这种状况,那我必须说这跟 React 的实现细节耦合地太紧密了,根本不能做为有效参考用例。

我也但愿 API 文档中有关 setState() 的章节可以加上“ state 和声明周期”这一深度指南的连接,这能给那些想要全面学习 setState() 的用户更多的细节内容。setState() 并不是同步操做,也无任何有意义的返回结果,仅仅是简单地描述其函数签名而没有深刻地探讨其各类影响和表现,这对初学者是极不友好的。

初学者必须花上大量时间去找出问题:Google 上搜、StackOverflow 上搜、GitHub issues 里搜。

setState() 为什么如此严苛?

setState() 的怪异表现并不是 bug,而是特性。实际上,甚至能够说这是 React 之因此存在的根本缘由。

React 的一大创做动机就是保证肯定性渲染:给定应用 state ,渲染出特定结果。理想状况下,给定 state 相同,渲染结果也应相同。

为了达到此目的,当发生变化时,React 经过采起一些限制性手段来管理变化。咱们不能随意取得某些 DOM 节点而后就地修改之。相反,React 负责 DOM 渲染;当 state 发生改变时,也由React 决定如何重绘。咱们不渲染 DOM,而是由 React 来负责。

为了不在 state 更新的过程当中触发重绘,React 引入了一条规则:

React 用于渲染的 state 不能在 DOM 渲染的过程当中发生改变。咱们不能决定组件 state 什么时候获得更新,而是由 React 来决定。

困惑就此而来。当你调用 setState() 时,你觉得你设置了 state ,其实并无。

“你就接着装逼,你觉得你因此为的就是你因此为的吗?”

什么时候使用 setState()?

我通常只在不须要持久化 state 的自包含功能单元中使用 setState(),例如可复用的表单校验组件、自定义的日期或时间选择部件(widget)、可自定义界面的数据可视化部件等。

我称这种组件为“小部件(widget)”,它们通常由两个或两个以上组件构成:一个负责内部 state 管理的容器组件,一个或多个负责界面显示的子组件

几条立见分晓的检验方法(litmus tests):

  • 是否有其余组件是否依赖于该 state ?
  • 是否须要持久化 state ?(存储于 local storage 或服务器)

若是这两个问题的答案都是“否”的话,那使用 setState() 基本是没问题的;不然,就要另做考虑了。

据我所知,Facebook 使用受管于 Relay containersetState() 来包装 Facebook UI 的各个不一样部分,例如大型 Facebook 应用内部的迷你型应用。于 Facebook 而言,以这种方式将复杂的数据依赖和须要实际使用这些数据的组件放在一块儿是很好的。

对于大型(企业级)应用,我也推荐这种策略。若是你的应用代码量很是大(十万行以上),那此策略多是很好的 —— 但这并不意味着这种方式就不能应用于小型应用中。

相似地,并不意味着你不能将大型应用拆分红多个独立的迷你型应用。我本身就结合 Redux为企业级应用这样作过。例如,我常常将分析面板、消息管理、系统管理、团队/成员角色管理以及帐单管理等模块拆分红多个独立的应用,每一个应用都有其本身的 Redux store。经过 API tokens 和 OAuth,这些应用共享同一个域下的登陆/session 管理,感受就像是一个统一的应用。

对于大多数应用,我建议默认使用 Redux。须要指出的是,Dan Abramov(Redux 的做者)在这一点上和我持相反的观点。他喜欢应用尽量地保持简单,这固然没错。传统社区有句格言如是说:“除非真得感到痛苦,不然就别用 Redux”。

而个人观点是:

“不知道本身正走在黑暗中的人是永远不会去搜寻光明的“。

正如我说过的,在某些状况下,Redux 比 setState() 更简单。经过消除一切和共享的可变 state 以及同步依赖有关的 bug,Redux 简化了 state 管理问题。

setState() 确定要学,但即便你不想使用 Redux,你也应该学学 Redux。不管你采用何种解决方案,它都能让你重新的角度思考去应用的 state 管理问题,也可能能帮你简化应用 state。

对于有大量衍生(derived ) state 的应用而言, MobX 可能会比 setState() 和 Redux 都要好,由于它很是擅于高效地管理和组织须要经过计算获得的(calculated ) state 。

得利于其细粒度的、可观察的订阅模型,MobX也很擅于高效渲染大量(数以万计)动态 DOM 节点。所以,若是你正在开发的是一款图形游戏,或者是一个监控全部企业级微服务实例的控制台,那 MobX 多是个很好的选择,它很是有利于实时地可视化展现这种复杂的信息。

接下来

想要全面学习如何用 React 和 Redux 开发软件?

跟着 Eric Elliott 学 Javacript,机不可失时再也不来!

Eric Elliott 是 “编写 JavaScript 应用” (O’Reilly) 以及 “跟着 Eric Elliott 学 Javascript” 两书的做者。他为许多公司和组织做过贡献,例如 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN和BBC等 , 也是不少机构的顶级艺术家,包括但不限于 Usher , Frank Ocean , Metallica。

大多数时间,他都在 San Francisco Bay Area,同这世上最美的女子在一块儿(译注:这是怕老婆呢仍是怕老婆呢仍是怕老婆呢?)。




在 setState 中使用函数替代对象

React 文档 最近改版了——若是你还没看过,你的确应该去看看!经过写一份“React 术语词典”我愈来愈有豁然开朗的感受了,其过程当中我也深刻地通读了新文档的所有内容。阅读文档的时候,我发现了 setState 相对鲜为人知的一面,并由这个推文大受启发:

我想我要写一篇博文来解释其原理。

先介绍一下背景

React 中的组件是独立、可重用的代码块,它们常常有本身的状态。组件返回的 React 元素组成了应用的 UI 界面。含有本地状态的组件会有一个名为 state 的属性,当咱们想要改变应用的外观或表现形式时,咱们须要改变组件的状态。那么咱们如何更新组件的状态呢?React 组件中有一个可用的方法叫作 setState,它经过调用 this.setState 来使得 React 从新渲染你的应用并更新 DOM。

一般更新组件的时候,咱们只要调用 setState 函数并以对象的形式传入一个新的值:this.setState({someField:someValue})

可是常常会须要使用当前状态去更新组件的状态,直接访问 this.state 来更新组件到下一个状态并是不可靠的方式。根据 React 的文档:

由于 this.propsthis.state 存在异步更新的可能,你不该该根据这些值计算下一个状态。

文档中的关键词是异步!当调用 this.setState 时,DOM 并不能立刻更新,React 会分批次地更新,这样才能更高效地从新渲染全部的组件。

示例

咱们来看一下在 Shopsifter 中使用 setState 的典型例子(我用于收集反馈信息),在用户提交他/她的反馈信息以后,页面会显示感谢信息以下:

反馈页面的组件拥有一个布尔值的 showForm 属性,该值决定了应该显示表单仍是感谢信息。个人反馈表单组件的初始化状态是这样的:

this.state = { showForm : true}
复制代码

而后,当用户点击了提交按钮,我调用了这个函数:

submit(){
  this.setState({showForm : !this.state.showForm});
}
复制代码

我依赖于 this.state.showForm的值来改变表单的下一个状态。这个简单的例子中,依赖这个值可能并不会致使任何问题,可是想象一下,当一个应用变得更加复杂,会有不少次调用 setState 并依次将数据渲染至 DOM ,可能 this.state.showForm的实际状态并非你所认为的样子。

若是咱们不依赖于 this.state 来计算下一个值,咱们该怎样作呢?

setState 中的函数来拯救你了!

咱们能够向 this.setState 传入一个函数来替代传入对象,而且能够可靠地获取组件的当前状态。上文的提交函数如今是这样写的:

submit(){
   this.setState(function(prevState, props){
      return {showForm: !prevState.showForm}
   });

}
复制代码

经过使用函数替代对象传入 setState 的方式可以获得组件的 stateprops 属性可靠的值。值得注意的一点是,在 React 文档的例子中使用了箭头函数(这也是我将要应用到个人 Shopsifter 应用中的一项内容),所以上文的例子中个人函数使用的仍然是 ES5 的语法。

若是你知道本身将要使用 setState 来更新组件,而且你知道本身将要使用当前组件的状态或者属性值来计算下一个状态,我推荐你传入一个函数做为 this.setState 的第一个参数而不用对象的解决方案。

我但愿这能帮助你作出更好、更可靠的 React 应用!





更合理的 setState()

原文发表在个人博客:www.erichain.me/2017/04/17/…

React 是我作前端以来接触到的第三个框架(前两个分别是 Angular 和 Vue),不管是从开发体验上和效率上,这都是一门很是优秀的框架,很是值得学习。

原谅我说了一些废话,如下是正文。

借助于 Redux,咱们能够轻松的对 React 中的状态进行管理和维护,同时,React 也为咱们提供了组件内的状态管理的方案,也就是 setState()。本文不会涉及到 Redux,咱们将从 Component 的角度来讲明你不知道的以及更合理的 setState()

先说说你们都知道的

在 React 文档的 State and Lifecycle 一章中,其实有明确的说明 setState() 的用法,向 setState() 中传入一个对象来对已有的 state 进行更新。

好比如今有下面的这样一段代码:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: this.state.count + 1
    };
  }
}复制代码

咱们若是想要对这个 state 进行更新的话,就能够这样使用 setState()

this.setState({
  count: 1
});复制代码

你可能不知道的

最基本的用法世人皆知,可是,在 React 的文档下面,还写着,处理关于异步更新 state 的问题的时候,就不能简单地传入对象来进行更新了。这个时候,须要采用另一种方式来对 state 进行更新。

setState() 不只可以接受一个对象做为参数,还可以接受一个函数做为参数。函数的参数即为 state 的前一个状态以及 props。

因此,咱们能够向下面这样来更新 state:

this.setState((prevState, props) => ({ count: prevState.count + 1 }));复制代码

这样写的话,可以达到一样的效果。那么,他们之间有什么区别呢?

区别

咱们来详细探讨一下为何会有两种设置 state 的方案,他们之间有什么区别,咱们应该在什么时候使用何种方案来更新咱们的 state 才是最好的。

此处,为了可以明确的看出 state 的更新,咱们采用一个比较简单的例子来进行说明。

咱们设置一个累加器,在 state 上设置一个 count 属性,同时,为其增长一个 increment 方法,经过这个 increment 方法来更新 count

此处,咱们采用给 setState() 传入对象的方式来更新 state,同时,咱们在此处设置每调用一次 increment 方法的时候,就调用两次 setState()。具体的缘由咱们在后文中会讲解。

具体的代码以下:

class IncrementByObject extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.increment = this.increment.bind(this);
  }

  // 此处设置调用两次 setState()
  increment() {
    this.setState({
      count: this.state.count + 1
    });

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

  render() {
    return (
      <div>
        <button onClick={this.increment}>IncrementByObject</button>
        <span>{this.state.count}</span>
      </div>
    );
  }
}

ReactDOM.render(
  <IncrementByObject />,
  document.getElementById('root')
);复制代码

这时候,咱们点击 button 的时候,count 就会更新了。可是,可能与咱们所预期的有所差异。咱们设置了点击一次就调用两次 setState(),可是,count 每一次却仍是只增长了 1,因此这是为何呢?

其实,在 React 内部,对于这种状况,采用的是对象合并的操做,就和咱们所熟知的 Object.assign() 执行的结果同样。

好比,咱们有如下的代码:

Object.assign({}, { a: 2, b: 3 }, { a: 1, c: 4 });复制代码

那么,咱们最终获得的结果将会是 { a: 1, b: 3, c: 4 }。对象合并的操做,属性值将会以最后设置的属性的值为准,若是发现以前存在相同的属性,那么,这个属性将会被后设置的属性所替换。因此,也就不难理解为何咱们调用了两次 setState() 以后,count 依然只增长了 1 了。

用简短的代码说明就是这样:

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

// 同理于
Object.assign({}, this.state, { count: this.state.count + 1 });复制代码

以上是咱们采用对象的方式传入 setState() 来更新 state 的说明。接下来咱们再看看使用函数的方式来更新 state 会有怎么样的效果呢?

咱们将上面的累加器采用另外的方式来实现一次,在 setState() 的时候,咱们采用传入一个函数的方式来更新咱们的 state。

class IncrementByFunction extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.increment = this.increment.bind(this);
  }

  increment() {
    // 采用传入函数的方式来更新 state
    this.setState((prevState, props) => ({
      count: prevState.count + 1
    }));
    this.setState((prevState, props) => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return (
      <div>
        <button onClick={this.increment}>IncrementByFunction</button>
        <span>{this.state.count}</span>
      </div>
    );
  }
}

ReactDOM.render(
  <IncrementByFunction />,
  document.getElementById('root')
);复制代码

当咱们再次点击按钮的时候,就会发现,咱们的累加器就会每次增长 2 了。

咱们能够经过查看 React 的源代码来找出这两种更新 state 的区别 (此处只展现经过传入函数进行更新的方式的部分源码)。

在 React 的源代码中,咱们能够看到这样一句代码:

this.updater.enqueueSetState(this, partialState, callback, 'setState');复制代码

而后,enqueueSetState 函数中又会有这样的实现:

queue.push(partialState);
enqueueUpdate(internalInstance);复制代码

因此,与传入对象更新 state 的方式不一样,咱们传入函数来更新 state 的时候,React 会把咱们更新 state 的函数加入到一个队列里面,而后,按照函数的顺序依次调用。同时,为每一个函数传入 state 的前一个状态,这样,就能更合理的来更新咱们的 state 了。

问题所在

那么,这就是传入对象来更新 state 会致使的问题吗?固然,这只是问题之一,还不是主要的问题。

咱们以前也说过,咱们在处理异步更新的时候,须要用到传入函数的方式来更新咱们的 state。这样,在更新下一个 state 的时候,咱们可以正确的获取到以前的 state,并在在其基础之上进行相应的修改。而不是简单地执行所谓的对象合并。

因此说,咱们建议,在使用 setState 的时候,采用传入函数来更新 state 的方式,这样也是一个更合理的方式。

相关文章
相关标签/搜索