[译] setState() 门事件

React setState() 解惑

译注:本文原由于做者的一条推特,他认为应该避免使用 setState(),随后引起论战,遂写此文详细阐明其观点。译者我的认为,本文主要在于“撕逼“,并未深刻介绍 setState() 的技术细节,但愿从技术层面深刻了解 setState() 的同窗能够参考[译] React 将来之函数式 setState。对 setState() 不了解的同窗可能会感到本文不知所云,特此说明。javascript

一切都源于上周。3 位 React 初学者尝试在项目中使用 setState() 时遇到了 3 种不一样的问题。我指导过不少 React 新手,也为团队提供从其余技术到 React 的架构转型咨询。html

其中一位初学者正在开发一个十分适合使用 Redux 的生产项目,因此我没有正面去解决 setState() 的同步问题(the timing with setState()),而是直接建议他用 Redux 替换掉 setState(),由于使用 Redux 能避免 state 在组件渲染的过程当中发生改变。Redux 简单地利用来自 store 的 props 来决定如何渲染界面,巧妙地规避了复杂的同步问题。java

所以也就有了下面这条推特:react

“React 有个 setState() 问题:让新手使用 setState() 毫无好处(a recipe for headaches)。高手们已经学会了如何避免使用它"git

以后,有些高手就来纠正我了:github

“我是 React 团队的一员。在尝试其余方法以前,请学会使用 setState。”编程

“那些所谓‘高手’们怕是要落伍了,由于 React 17 将会默认采用异步调度。”redux

对于第二点:vim

“Fiber 有一种用于暂停、切分、重建和取消更新的策略,但若是你脱离了组件 state,那此策略便没法正常工做了。”c#

貌似都没错,但是码农们就要骂娘了:

面对困境“呵呵”两下并没有妨,不过千万别呵呵事后就对问题视而不见了。

在和另外一个初学者交流的时候,我发现他也对 setState() 的工做机制感到困惑。他后来索性放弃了,他把 state 塞在一个闭包里;显而易见,闭包中 state 的改变是不会触发 render 函数自动执行的。

考虑到深感困惑的初学者之多,我仍是坚持我上述推文中前半句的观点;但若是能够重来的话,我会对后半句稍做修改,由于确有不少高手在(主要是 Facebook 和 Netfix 的工程师)大量地使用 setState()

“React 有个 setState() 问题:叫新手使用 setState() 毫无好处,但高手们自有神技。“

固然,推特仍是有可能会丧失其集体智慧(lose its collective mind)(译注:我的认为这句应该是指当网络上大多数人持某一观点时,那即便该观点是错的,那你也不能指出其错误,不然就会招致集体攻讦;或者说,真理有时候只掌握在少数人手里)。 毕竟,React 是“完美的”, 咱们都必须认可 setState的美妙优雅是多么的恰如其分,不然只会遭到冷嘲热讽。

若是 setState() 令你感到困惑,那都是你的问题 —— 你要么是疯子,要么是傻瓜。(我好像忘了说 Javascript 社区的霸凌问题了

好了,当你嘲笑全部初学者的时候,先检讨检讨本身吧,别觉得掌握了 setState() 就能够忘乎所以了。

那种行为是荒谬好笑的,是精英主义论的,会让新手们感到十分讨厌。若是人们常常对某个 API 感到困惑的话,那就该改进 API 自己的设计了,或者至少应该改进下文档。

让咱们的社区和工具变得更加友好对全部人来讲都是件好事。

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 SystemsZumba FitnessThe Wall Street JournalESPNBBC等 , 也是不少机构的顶级艺术家,包括但不限于 Usher , Frank Ocean , Metallica

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

相关文章
相关标签/搜索