函数式的 setState 是 React 的未来

6,019 阅读11分钟

本文翻译自: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.propsthis.state可以异步更新,所以不应该依赖它们的值来计算下一个状态。

索菲亚·舒梅克(Sophia Shoemaker)的这个例子可以演示这个问题。 演示它,并注意这个例子中的坏和好的解决方案。

函数式setState解决了我们的问题

如果你没有花时间演示上面的例子,我强烈建议你还是先看一下,因为它将帮助你掌握这篇文章的核心概念。

当你演示了上面的例子,你无疑看到函数式setState解决了我们的问题。但究竟是怎么做的呢?

我们来咨询React的核心成员 - Dan。

Dan的twitter

请注意他给出的答案。

当你使用函数式setState ...

更新将被放进一个队列,然后按调用顺序执行。

因此,当React遇到“多次函数式setState()调用”时,React按照“调用它们的顺序”对函数进行排队,而不是将对象合并在一起,(当然,并没有要合并的对象)。

之后,React继续通过调用“队列”中的每个函数来更新状态,将它们传递给先前的状态 - 即,在第一个函数setState()调用之前的状态(如果当前是第一个函数setState()正在执行)或队列中前一个函数setState()调用的最新更新的状态。

下面我们将来模拟一个setState()方法,这是为了让你了解React正在做什么。另外,为了减少冗长,我们将使用ES6。如果需要,您随时可以编写ES5版本。

首先,让我们创建一个组件类。然后,在其中,我们将创建一个假的setState()方法。此外,我们的组件将具有increaseScoreBy3()方法,该方法将执行多功能setState。最后,我们会像 React 所做的那样实例化该类。

class User{
  state = {score : 0};
  //let's fake 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 在更新状态后调用它。

现在,当用户触发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 setState
  setState(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 class
  handleIncreaseScore () {
    this.setState( increaseScore)
  }
  ...
}

这是声明性的!您的组件类不再关心状态更新。它只是声明它想要的更新类型。

要深刻理解这一点,请考虑那些通常具有许多状态切片的复杂组件,在不同操作更新每个切片。有时,每个更新功能都需要多行代码。所有这些逻辑都将存在于您的组件中。但以后不再是这样了!

另外,我喜欢让每个模块都尽可能短。如果你像我一样觉得你现在的模块太长了,您可以将所有状态更改逻辑提取到其他模块,然后导入并在组件中使用它。

import {increaseScore} from "../stateChanges";
class User{
  ...
  // inside your component class
  handleIncreaseScore () {
    this.setState( increaseScore)
  }
  ...
}

现在,您甚至可以在另一个组件中重用increaseScore函数,只需导入它。

你还可以用函数式setState做什么?

让测试变得简单!

这条推的地址

你也可以传递额外的参数来计算下一个状态(这个让我大吃一惊...... )

这条推的地址

期待更多......

React的未来

多年来,React团队一直在探索如何最好地实现有状态的函数。 函数式setState似乎正是正确的答案(可能)。

嘿,丹!最后(再说)一句话(来展望下 React)?

这条推的地址

如果你已经看到这里,你可能会像我一样兴奋。立即开始尝试使用函数式setState!

快乐撸码!