本文翻译自:Functional setState is the future of React – freeCodeCamp.org
译者注:昨天自己有遇到一个 setState 的坑,就是【React 踩坑记】setState 这样用可能会出错!这篇文章里记录的,上网 Google 了下看到这一篇,关于 setState,这篇文章讲解的很详细深入👍,所以翻译到掘金来,让更多人可以看到。
更新:我在React Rally上就此主题进行了后续讨论。虽然这篇文章更多的是关于“函数式的 setState”模式,但更多的是关于深入理解setState。
我在React Rally 上关于 setState 做的一个分享 Justice Mba - Demystifying setState() - YouTube
React在JavaScript中推广了函数式编程,这导致了大量的框架采用了React使用的基于组件的UI模式。如今,函数式热潮正在蔓延到整个 web 开发生态系统中。
译者注:上述内容翻译如下:
JavaScript生态系统正在从“本周的新框架”转变为“新的(更快的)本周的React克隆”
ReactJS新闻@ReactJSNews> 阿里巴巴发布了他们自己的类React框架,似乎是更轻量更快的 - 但是肯定有一个缺点!github.com/alibaba/rax
但React团队远没有放松。他们继续深入挖掘,探索更多的函数式宝石。
所以今天我向你透露一个隐藏在React中的函数式宝石 - 函数式 setState!
好吧,这个名字只是我刚刚编造的…而且这并不是全新的东西或秘密。不,不完全是。其实它是React内置的一种模式,只有很少有开发人员知道这种模式。 它从来没有名字,但现在它可以叫做 - 函数式 setState!
正如Dan Abramov所描述的,函数式 setState 就是一种这样的模式:
“与组件类分开声明状态更改。”
咦?
好吧…这些是你已经知道的了
React是一个基于组件的UI库。组件基本上是一个接受一些属性并返回UI元素的函数。
function User(props) {return (<div>A pretty user</div>);
}
组件可能需要拥有并管理其状态。在这种情况下,您通常将组件编写为类。然后你的状态存在于类的constructor
函数中:
class User {constructor () {this.state = {score : 0};}render () {return (<div>This user scored {this.state.score}</div>);}
}
为了管理状态,React提供了一个名为setState()
的特殊方法。用法如下:
class User {... increaseScore () {this.setState({score : this.state.score + 1});}...
}
请注意setState()
的工作原理。您传递一个包含要更新的 state 部分的对象。换句话说,您传递的对象将具有与组件 state 中的键对应的键,然后setState()
通过将对象合并到 state 来更新或设置 state。这就是“set-State”
你可能不知道的
还记得我们说的setState()
的工作原理吗?那么,如果我告诉你可以传递一个函数来代替传递一个对象呢?
是的。setState()
也接受一个函数来作为参数。该函数接受组件的先前 state 和 当前的 props,它用于计算并返回下一个 state。如下所示:
this.setState(function (state, props) {
return {score: state.score - 1
}
});
请注意,setState()
是一个函数,我们将另一个函数传递给它(函数式编程…函数式 setState)。乍一看,代码可能看起来很丑陋,只有设置状态的步骤太多了。但为什么还得这样做呢?
为什么要将函数传递给setState?
关键在于,状态更新可能是异步的。
想想调用setState()
时会发生什么。React将首先将传递给setState()
的对象合并到当前状态。然后它将开始合并。它将创建一个新的React Element树(UI的对象表示),将新树与旧树进行区分,根据传递给setState()
的对象找出已更改的内容,然后最终更新DOM。
呼!这么多工作!实际上,这甚至是一个简化过的总结。但是相信React:
React does not simply “set-state”.
由于涉及的工作量很大,调用setState()
可能不会立即更新您的状态。
React可以将多个
setState()
的调用批处理成单个更新来提高性能。
上面这句话是什么意思?
首先,“多次调用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});
}
现在,当React遇到“多次调用setState()
”,而不是整整三次执行“set-state”时,React将避免我上面描述的大量工作并巧妙地对自己说:“不! 我不打算愚公移山,每次都更新一些状态。我宁愿得到一个容器,将所有这些切片包装在一起,只需更新一次。“这就是批处理!
请记住,传递给setState()
的是一个普通对象。现在,假设任何时候React遇到“多次调用setState()
”,它通过提取传递给每个setState()
调用的所有对象来完成批处理,将它们合并在一起形成一个对象,然后使用该单个对象来执行setState()
。
在JavaScript中,合并对象可能如下所示:
const singleObject = Object.assign({}, objectFromSetState1, objectFromSetState2, objectFromSetState3
);
这种模式称为对象组合。
在JavaScript中,“合并”或组合对象的方式是:如果三个对象具有相同的键,则传递给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
的值。
因此,如果使用对象作为参数多次调用setState()
——每次传递一个对象——React将合并。换句话说,它将用我们传递的多个对象中组成一个新对象。 如果任何对象包含相同的键,则存储具有相同键的最后一个对象的键的值。对吧?
这意味着,鉴于我们上面的increaseScoreBy3
函数,函数的最终结果将只是1
而不是3
,因为 React 没有立即按我们调用setState()
的顺序更新状态。首先,React将所有对象组合在一起,结果如下:{score:this.state.score + 1}
,然后只使用新组合的对象进行“set-state”一次。 像这样:User.setState({score:this.state.score + 1}
。
要非常清楚,将对象传递给setState()
不是问题所在。真正的问题在于当你想要基于前一个状态计算下一个状态时,将对象传递给setState()
。所以停止这样做。这不安全!
因为
this.props
和this.state
可以异步更新,所以不应该依赖它们的值来计算下一个状态。
索菲亚·舒梅克(Sophia Shoemaker)的这个例子可以演示这个问题。 演示它,并注意这个例子中的坏和好的解决方案。
函数式setState解决了我们的问题
如果你没有花时间演示上面的例子,我强烈建议你还是先看一下,因为它将帮助你掌握这篇文章的核心概念。
当你演示了上面的例子,你无疑看到函数式setState解决了我们的问题。但究竟是怎么做的呢?
我们来咨询React的核心成员 - Dan。
请注意他给出的答案。
当你使用函数式setState …
更新将被放进一个队列,然后按调用顺序执行。
因此,当React遇到“多次函数式setState()
调用”时,React按照“调用它们的顺序”对函数进行排队,而不是将对象合并在一起,(当然,并没有要合并的对象)。
之后,React继续通过调用“队列”中的每个函数来更新状态,将它们传递给先前的状态 - 即,在第一个函数setState()调用之前的状态(如果当前是第一个函数setState()正在执行)或队列中前一个函数setState()调用的最新更新的状态。
下面我们将来模拟一个setState()方法,这是为了让你了解React正在做什么。另外,为了减少冗长,我们将使用ES6。如果需要,您随时可以编写ES5版本。
首先,让我们创建一个组件类。然后,在其中,我们将创建一个假的setState()方法。此外,我们的组件将具有increaseScoreBy3()方法,该方法将执行多功能setState。最后,我们会像 React 所做的那样实例化该类。
class User{state = {score : 0};//let's fake setStatesetState(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 在更新状态后调用它。
现在,当用户触发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);
没错,这不是一个很棒的代码,你肯定可以写出更好的代码。但这里的关键焦点是每次 React 执行函数 setState 中的函数时,React 都会通过向其传递更新 state 的新副本来更新您的状态。这使得函数 setState 可以基于前一次的 state 来设置新的 state。 在这里,我用完整的代码创建了一个bin。
我把这个例子补充完整,便于你们可以更好地理解它。
class User{state = {score : 0};//fake setStatesetState(state, callback) {console.log("state", state);this.state = Object.assign({}, this.state, state);if (callback) callback();}
}
const Justice = new User();
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)) );
}
运行一下这段代码,确保你看懂它。当你回来时我们会看到是什么让函数式的setState真正变得闪闪发光。
这个秘诀我只告诉你哦
到目前为止,我们已经深入探讨了为什么在React中执行多个函数式setStates是安全的。但是我们实际上还没有完成函数式setState的完整定义:“声明状态更改与组件类分开”。
多年来,setting-state 的逻辑——即我们传递给setState()的函数或对象 - 总是存在于组件类中,这更像是命令式的而非声明式的。
那么今天,我向你展示新出土的宝藏 - 最好的React秘密:
感谢Dan Abramov!
这是函数式setState的强大功能。在组件类之外声明状态更新逻辑。然后在组件类中调用它。
// outside your component class
function increaseScore (state, props) {return {score : state.score + 1}
}
class User{...
// inside your component classhandleIncreaseScore () {this.setState( increaseScore)}...
}
这是声明性的!您的组件类不再关心状态更新。它只是声明它想要的更新类型。
要深刻理解这一点,请考虑那些通常具有许多状态切片的复杂组件,在不同操作更新每个切片。有时,每个更新功能都需要多行代码。所有这些逻辑都将存在于您的组件中。但以后不再是这样了!
另外,我喜欢让每个模块都尽可能短。如果你像我一样觉得你现在的模块太长了,您可以将所有状态更改逻辑提取到其他模块,然后导入并在组件中使用它。
import {increaseScore} from "../stateChanges";
class User{...// inside your component classhandleIncreaseScore () {this.setState( increaseScore)}...
}
现在,您甚至可以在另一个组件中重用increaseScore函数,只需导入它。
你还可以用函数式setState做什么?
让测试变得简单!
你也可以传递额外的参数来计算下一个状态(这个让我大吃一惊… )
期待更多…
React的未来
多年来,React团队一直在探索如何最好地实现有状态的函数。 函数式setState似乎正是正确的答案(可能)。
嘿,丹!最后(再说)一句话(来展望下 React)?
如果你已经看到这里,你可能会像我一样兴奋。立即开始尝试使用函数式setState!
快乐撸码!