- 原文地址:Functional setState is the future of React
- 原文做者:Justice Mba
- 译文出自:掘金翻译计划
- 译者:reid3290
- 校对者:sunui,imink
React 使得函数式编程在 JavaScript 领域流行了起来,这驱使大量框架采用 React 所推崇的基于组件的编程模式,函数式编程热正在大范围涌向 web 开发领域。javascript
可是 React 团队却还不“消停”,他们持续深耕,从 React(已经超神了!)中发掘出更多函数式编程的宝藏。html
所以本文将展现深藏在 React 中的又一函数式“宝藏” —— 函数式(functional)setState!前端
好吧,名字实际上是我乱编的,并且这个技术也称不上是新事物或者是个秘密。这一模式内建于 React 中,可是只有少数 React 深耕者才知道,并且从未有过正式名称 —— 不过如今它有了,那就是函数式 setState!java
正如 Dan Abramov 所言,在函数式 setState 模式中,“组件 state 变化的声明能够和组件类自己独立开来”。react
这?android
React 是一个基于组件的 UI 库,组件基本上能够看做是一个接受某些属性而后返回 UI 元素的函数。ios
function User(props) {
return (
<div>A pretty user</div>
);
}复制代码
组件可能须要持有并管理其 state。在这种状况下,通常将组件编写为一个类,而后在该类的 constructor
函数中初始化 state:git
class User {
constructor () {
this.state = {
score : 0
};
}
render () {
return (
<div>This user scored **{this.state.score}**</div>
);
}
}复制代码
React 提供了一个用于管理 state 的特殊函数 —— setState()
,其用法以下:github
class User {
...
increaseScore () {
this.setState({score : this.state.score + 1});
}
...
}复制代码
注意 setState()
的做用机制:你传递给它一个对象,该对象含有 state 中你想要更新的部分。换句话说,该对象的键(keys)和组件 state 中的键相对应,而后 setState()
经过将该对象合并到 state 中来更新(或者说 sets)state。所以称为 “set-State”。web
记住 setState()
的做用机制了吗?若是我告诉你说,setState()
不只能接受一个对象,还能接受一个函数做为参数呢?
没错,setState()
确实能够接受一个函数做为参数。该函数接受该组件前一刻的 state 以及当前的 props 做为参数,计算和返回下一刻的 state。以下所示:
this.setState(function (state, props) {
return {
score: state.score - 1
}
});复制代码
注意 setState()
自己是一个函数,并且咱们传递了另外一个函数给它做为参数(函数式编程,函数式 setState)。乍一看可能以为这样写挺丑陋的,set-state 须要的步骤太多了。那为何还要这样写呢?
理由是,state 的更新多是异步的。
思考一下调用 setState()
时发生了什么。React 首先会将你传递给 setState()
的参数对象合并到当前 state 对象中,而后会启动所谓的 reconciliation,即建立一个新的 React Element tree(UI 层面的对象表示),和以前的 tree 做比较,基于你传递给 setState()
的对象找出发生的变化,最后更新 DOM。
呦!工做不少嘛!实际上,这还只是精简版总结。但必定要相信:
React 不会仅仅简单地 “set-state”。
考虑到所涉及的工做量,调用 setState()
并不必定会即时更新 state。
考虑到性能问题,React 可能会将屡次
setState()
调用批处理(batch)为一次 state 的更新。
这又意味着什么呢?
首先,“屡次 setState()
调用” 的意思是说在某个函数中调用了屡次 setState()
,例如:
...
state = {score : 0};
// 屡次 setState() 调用
increaseScoreBy3 () {
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
}
...复制代码
面对这种 屡次 setState()
调用 的状况,为了不重复作上述大量的工做,React 并不会真地完整调用三次 "set-state";相反,它会机智地告诉本身:“哼!我才不要‘愚公移山’三次呢,每次还得更新部分 state。不行,我得找个‘背包’,把这些部分更新打包装好,一次性搞定。”朋友们,这就是所谓的批处理啊!
记住传递给 setState()
的纯粹是个对象。如今,假设 React 每次遇到 屡次 setState()
调用都会做上述批处理过程,即将每次调用 setState()
时传递给它的全部对象合并为一个对象,而后用这个对象去作真正的 setState()
。
在 JavaScript 中,对象合并能够这样写:
const singleObject = Object.assign(
{},
objectFromSetState1,
objectFromSetState2,
objectFromSetState3
);复制代码
这种写法叫做 object 组合(composition)。
在 JavaScript 中,对象“合并(merging)”或者叫对象组合(composing)的工做机制以下:若是传递给 Object.assign()
的多个对象有相同的键,那么最后一个对象的值会“胜出”。例如:
const me = {name : "Justice"},
you = {name : "Your name"},
we = Object.assign({}, me, you);
we.name === "Your name"; //true
console.log(we); // {name : "Your name"}复制代码
由于 you
是最后一个合并进 we
中的,所以 you
的 name
属性的值 “Your name” 会覆盖 me
的 name
属性的值。所以 we
的 name
属性的值最终为 “Your name”,因此说 you
胜了!
综上所述,若是你屡次调用 setState()
函数,每次都传递给它一个对象,那么 React 就会将这些对象合并。也就是说,基于你传进来的多个对象,React 会组合出一个新对象。若是这些对象有同名的属性,那么就会取最后一个对象的属性值,对吧?
这意味着,上述 increaseScoreBy3
函数的最终结果会是 1 而不是 3。由于 React 并不会按照 setState()
的调用顺序即时更新 state,而是首先会将全部对象合并到一块儿,获得 {score : this.state.score + 1}
,而后仅用该对象进行一次 “set-state”,即 User.setState({score : this.state.score + 1}
。
须要搞清楚的是,给 setState()
传递对象自己是没有问题的,问题出在当你想要基于以前的 state 计算出下一个 state 时还给 setState()
传递对象。所以可别这样作了,这是不安全的!
由于
this.props
和this.state
多是异步更新的,你不能依赖这些值计算下一个 state。
下面 Sophia Shoemaker 写的一个例子展现了上述问题,细细把玩一番吧,留意其中好坏两种解决方案。
若是你还不曾把玩上面的例子,我仍是强烈建议你玩一玩,由于这有利于你理解本文的核心概念。
在把玩上述例子的时候,你确定注意到了 setState 解决了咱们的问题。但到底是如何解决的呢?
让咱们请教一下 React 界的 Oprah(译者注:非知名脱口秀主持人)—— Dan。
注意看他给出的答案,当你编写函数式 setState 的时候,
更新操做会造成一个任务队列,稍后会按其调用顺序依次执行。
所以,当面对屡次函数式 setState()
调用时,React 并不会将对象合并(显然根本没有对象让它合并),而是会按调用顺序将这些函数排列起来。
以后,React 会依次调用队列中的函数,传递给它们前一刻的 state —— 若是当前执行的是队列中的第一个函数式 setState()
,那么就是在该函数式 setState()
调用以前的 state;不然就是最近一次函数式 setState()
调用并更新了 state 以后的 state。经过这种机制,React 达到 state 更新的目的。
话说回来,我仍是以为代码更有说服力。只不过此次咱们会“伪造”点东西,虽然这不是 React 内部真正的作法,但也基本是这么个意思。
还有,考虑到代码简洁问题,下面会使用 ES6,固然你也能够用 ES5 重写一下。
首先,建立一个组件类。在这个类里,建立一个伪造的 setState()
方法。该组件会使用 increaseScoreBy3()
方法来屡次调用函数式 setState。最后,会仿照 React 的作法实例化该类。
class User{
state = {score : 0};
//“伪造” setState
setState(state, callback) {
this.state = Object.assign({}, this.state, state);
if (callback) callback();
}
// 屡次函数式 setState 调用
increaseScoreBy3 () {
this.setState( (state) => ({score : state.score + 1}) ),
this.setState( (state) => ({score : state.score + 1}) ),
this.setState( (state) => ({score : state.score + 1}) )
}
}
const Justice = new User();复制代码
注意 setState 还有一个可选的参数 —— 一个回调函数,若是传递了这个参数,那么 React 就会在 state 更新后调用它。
如今,当用户调用 increaseScoreBy3()
后,React 会将屡次函数式 setState 调用排成一个队列。本文旨在阐明为何函数式 setState 是安全的,所以不会在此模拟上述逻辑。但能够想象,所谓“队列化”的处理结果应该是一个函数数组,相似于:
const updateQueue = [
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1})
];复制代码
最后模拟更新过程:
// 按序递归式更新 state
function updateState(component, updateQueue) {
if (updateQueue.length === 1) {
return component.setState(updateQueue[0](component.state));
}
return component.setState(
updateQueue[0](component.state),
() =>
updateState( component, updateQueue.slice(1))
);
}
updateState(Justice, updateQueue);复制代码
诚然,这些代码并不能称之为优雅,你确定能写得更好。但核心概念是,使用函数式 setState,你能够传递一个函数做为其参数,当执行该函数时,React 会将更新后的 state 复制一份并传递给它,这便起到了更新 state 的做用。基于上述机制,函数式 setState 即可基于前一刻的 state 来更新当前 state。
下面是这个例子的完整代码,请细细把玩以充分理解上述概念(或许还能够改得更优雅些)。
一番把玩事后,让咱们来弄清为什么将函数式 setState 称之为“宝藏”。
至此,咱们已经深刻探讨了为何屡次函数式 setState 在 React 中是安全的。可是咱们尚未给函数式 setState 下一个完整的定义:“独立于组件类以外声明 state 的变化”。
过去几年,setting-state 的逻辑(即传递给 setState()
的对象或函数)一直都存在于组件类内部,这更像是命令式(imperative)而非 声明式(declarative)。(译者注:imperative 和 declarative 的区别参见 stackoverflow上的问答)
不过,今天我将向你展现新出土的宝藏 —— React 最为深藏不露的秘密:
感谢 Dan Abramov!
这就是函数式 setState 的强大之处 —— 在组件类外部声明 state 的更新逻辑,而后在组件类内部调用之。
// 在组件类以外
function increaseScore (state, props) {
return {score : state.score + 1}
}
class User{
...
// 在组件类以内
handleIncreaseScore () {
this.setState(increaseScore)
}
...
}复制代码
这就叫作 declarative!组件类不用再关心 state 该如何更新,它只须声明它想要的更新类型便可。
为了充分理解这样作的优势,不妨设想以下场景:你有一些很复杂的组件,每一个组件的 state 都由不少小的部分组成,基于 action 的不一样,你必须更新 state 的不一样部分,每个更新函数都有不少行代码,而且这些逻辑都存在于组件内部。不过有了函数式 setState,不再用面对上述问题了!
此外,我我的偏心小而美的模块;若是你和我同样,你就会以为如今这模块略显臃肿了。基于函数式 setState,你就能够将 state 的更新逻辑抽离为一个模块,而后在组件中引入和使用该模块。
import {increaseScore} from "../stateChanges";
class User{
...
// 在组件类以内
handleIncreaseScore () {
this.setState(increaseScore)
}
...
}复制代码
并且你还能够在其余组件中复用 increaseScore 函数 —— 只须引入模块便可。
函数式 setState 还能用于何处呢?
简化测试!
你还能够传递额外的参数用于计算下一个 state(这让我脑洞大开...#funfunFunction)。
更多精彩,敬请期待...
最近几年,React 团队一直都致力于更好地实现 stateful functions。
函数式 setState 看起来就是这个问题的正确答案(也许吧)。
Hey, Dan!还有什么最后要说的吗?
若是你阅读至此,估计就会和我同样兴奋了。即刻开始体验函数式 setState 吧!
欢迎扩散,欢迎吐槽(Twitter)。
Happy Coding!
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。