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。

常规方式

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

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可能得到的是旧的值。

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

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

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

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

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

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});
}
复制代码

顺眼多了。

函数方式

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 的方式,这样也是一个更合理的方式。


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值