Pluto's Realm

functional setState 是 React 的未来 (译文)

原文地址

React 已经让函数式编程在 JavaScript 中流行开来了。这也已经让许多重要的框架接受了 React 所使用的基于组件的 UI 模式。现在函数式已经在 web 开发生态中蔓延开来了。

但是 React 团队依然没减弱的势头。他们继续在深入挖掘,在探索着隐藏在这个库中的更有用的东西。

今天作者给我们揭开了隐藏在 React 中的宝藏 – 函数式 setState。

名字不重要,函数式大家肯定都听说过。它是内嵌在 React 中的一种模式,但是只有少数的开发者知道。

借用他人的话来描述这种模式:

把状态改变的声明和组件独立开来。

我们现在知道些什么

React 是一种基于 UI 库的组件。一个组件一般来说就是一个接受一些属性,并且返回一个 UI 元素的函数。

1
2
3
4
5
function User(props) {
return (
<div>A pretty</div>
)
}

组件可能需要保留或者管理它的状态。 所以,我们经常把组件写成一个类。这样一来我们就可以在类的 constructor 函数里定义它的状态了。

1
2
3
4
5
6
7
8
9
10
11
12
13
class User {
constructor() {
this.state = {
score: 0
}
}
render() {
return (
<div>This user scored {this.state.score}</div>
)
}
}

为了管理这些状态, React 提供了一个叫做 setState 的方法, 我们这样来使用它

1
2
3
4
5
6
7
8
class User {
...
increateScore() {
this.setState({score: this.state.score + 1});
}
...
}

注意 setState 是如何工作的。我们传递一个包含我们想修改的状态的对象作为它的参数。 换句话说,我们传递的对象应该有和组件状态对应的键, 这样 setState() 通过合并状态和这个对象来更新或者设置组件状态。

我们可能不知道什么

setState 除了接受一个对象作为参数,还可以接受一个函数。这个函数接受之前组件的 state 和现在 props 来计算和返回下一个 state:

1
2
3
4
5
this.setState(function (state, props) {
return {
score: state.score - 1
}
});

注意 setState() 是一个函数, 而我们正在传递另一个函数给它(函数式编程..functional setState)。乍一看,可能有点丑陋,仅仅设置一个状态就这么麻烦,为什么我们还要用它?

为什么传递一个函数来 setState

原因是, 状态的改变可能是异步的

想想 调用 setState 后会发生什么。React 首先将把传递给 setState 的对象合并进现在的状态。然后它将开始进行 reconciliation。它会创建一个新的 React 元素树(一个表示你的 UI 的对象), 然后拿这个树和之前的旧的树做对比, 计算出哪里发生了改变,然后更新DOM。

说白了 React 的 setState 没你想象的那么简单

因为涉及到这么多的工作, 调用 setState 后可能不会马上更新你的状态。

对于多次的 setState 调用,React 可能会处理成一次更新来优化性能。

意味着什么?

首先, 多次 setState 调用可能意味着在一个函数里调用 setState 多次,像这样:

1
2
3
4
5
6
7
8
...
state = {score: 0};
//多次 setState 调用
increateScoreBy3() {
this.setState({score:this.state.score + 1});
this.setState({score:this.state.score + 1});
this.setState({score:this.state.score + 1});
}

当 React 碰到这种状况时,它不会执行3个完整的过程,而是批量处理(batching)。 它会将3次调用传递给 setState 的对象合并成一个对象,然后用这个对象来进行一次 setState 。

在 JavaScript 中合并对象看起来像这样:

1
2
3
4
5
6
const singleObject = Object.assign(
{},
objectFromSetState1,
objectFromSetState2,
objectFromSetState3,
);

这种模式大多被称为对象组合 (object composition)。

在 JavaScript 中,这种合并或者组合对象的工作原理是: 如果这几个对象有相同的键, 那后面的对象的值将覆盖前面对象的值,作为最后的合并结果。例如:

1
2
3
4
5
6
const me = {name: 'fat pluto'},
you = {name: 'slim pluto'},
we = Object.assign({},me,you);
we.name === 'slim pluto'; //true
console.log(we); //{name: 'slim pluto'}

具体请查看 MDN

因此, 如果你多次调用一个参数是对象的 setState, React 将会合并。换句话说, 它将我们多次传递的对象组合成为一个新的对象。如果这些对象有相同的键,那么最后的那个对象的数据将会保存。

这就意味着上面我们多次调用的结果是 1 而不是 3.因为 React 并不会马上更新状态,所以 3 次调用中 this.state.score 的值全是0, 最后的结果就是1了。

更明白点,传递对象给 setState 方法并不是问题所在,真正的问题是当你传递的对象的值是需要通过上一次的状态来计算的时候。所以不要继续这样做了,他是不安全的。

因为 this.props 和 this.state 的更新可能是异步的,你不该依靠他们来计算你下一次的状态

这里有一个 demo 说明了这个问题

functional setState 解救了这个问题

从刚刚的 demo 中可以看到 functional setState 解决了这个问题,但是它是如何解决的?

当我们以传递函数的方式来调用 setState 时,这些更新将会排入一个队列,作为他们已经执行过了

所以,当 React 碰到多次的 functional setState() 调用时,不同于合并对象的方式(当然这里没有对象要合并), React 将会把它们排列起来,不会马上执行。

然后, React 通过调用队列的函数来继续更新状态,并将他们传递给上一次状态–第一个 functional setState 调用之前的状态(如果执行的是第一个 functional setState ),或者是上一次执行完队列中更新函数后的状态。

我们用代码来说话。首先,我们创建一个组件类,然后在它里面,我们创建一个虚构的 setState 方法。同样的,我们的组件有一个 increateScoreBy3 方法,它将会执行多次的 functional setState。最后,我们将实例化这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User {
state = {score:0}
setState(state,callback) {
this.state = Object.assign({},this.state,state);
if (callback) callback();
}
//多次 functional setState 调用
increateScoreBy3() {
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 同样接受一个回调函数作为参数,它将在状态更新后调用。

现在当一个 user 触发 increateScoreBy3(), React 将多次的 functional setState 队列起来。 这里我们不会模拟这些细节,我们的目的是怎样让 functional setState 安全的执行。你可以把这个排列过程的结果想象成一个函数数组。像这样:

1
2
3
4
5
const updateQueue = [
(state) => ({score: state.score + 1}),
(state) => ({score: state.score + 1}),
(state) => ({score: state.score + 1})
];

最后,我们模拟下更新的过程:

1
2
3
4
5
6
7
8
9
10
11
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 副本给你的 state。这样就可以保证每一次 functional setState 都是基于上一次状态的。

React 所隐藏的秘密

现在为止,我们已经深入的探究了为什么在 React 中使用 functional setState 来改变状态是安全的了。但是我们还没有实现 functional setState 的完全定义:’把改变状态的声明和组件独立出来’。

一直以来,设置状态的逻辑都存在于组件内,这更像是命令式而非声明式的。今天,我们将揭开 React 所隐藏的秘密。

1
2
3
4
5
6
7
8
9
10
11
12
13
//你的组件外
function increateScore(state,props) {
return {score: state.score + 1}
}
class User {
...
//组件内
handleIncreaseScore() {
this.setState(increateScore)
}
...
}

现在就是声明式的了。我们的组件再也不关心状态怎么改变了。它只是简单的声明了它想要更新的类型。

深入一点,想象下那些复杂的组件,它们可能有很多状态片段,每一个片段的更新都依据不同的行为。并且有时候,每一个更新函数都可能包含了很多行的代码。以前所有的这些都将包含在组件内,但是现在再也不用了。

同样的,如果你喜欢尽可能的将模块写的精简些,但是现在你感觉你的模块写的太多太长了。现在你能将所有状态改变的逻辑从你的模块中抽出来,放到另一个模块中去,在需要使用它们的时候 再引用它们。

1
2
3
4
5
6
7
8
9
10
import { increateScore } from '../stateChanges';
class User {
...
//组件内
handleIncreaseScore() {
this.setState(increateScore)
}
...
}

你甚至可以在不同的组件中使用它了。

除此之外, functional setState 还将使测试变得简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function increment(state,props) {
return {
value: state.value + props.step,
};
}
function decrement(state,props) {
return {
value: state.value - props.step,
}
}
expect(increment({value: 0},{step: 5})).toBe(5);
expect(increment({value: 0},{step: -5})).toBe(-5);
expect(decrement({value: 0},{step: 5})).toBe(-5);
expect(decrement({value: 0},{step: -5})).toBe(5);

你还可以传递参数来计算下一次状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function multiplyBy(multiplier) {
return function update(state) {
return {
value: state.value * multiplier,
}
}
}
class Counter extends React.component {
state = {value: 10};
handleMultiplyByFive = () => {
this.setState(multiplyBy(5));
}
render() {
return {
<div>
<h1>{this.state.value}</h1>
<button onClick={this.handleMultiplyByFive}>x 5</button>
</div>
}
}
}

React 的展望

几年来,React 团队一直在实验最好的实现方式

functional setState 可能是最正确的答案。所以开始搞起来吧。

PS: 奉上 Dan Abramov Twitter 中的讨论