加载中

React has popularized functional programming in JavaScript. This has led to giant frameworks adopting the Component-based UI pattern that React uses. And now functional fever is spilling over into the web development ecosystem at large.

ut the React team is far from relenting. They continue to dig deeper, discovering even more functional gems hidden in the legendary library.

So today I reveal to you a new functional gold buried in React, best kept React secret?—?Functional setState!

Okay, I just made up that name… and it’s not entirely new or a secret. No, not exactly. See, it’s a pattern built into React, that’s only known by few developers who’ve really dug in deep. And it never had a name. But now it does?—?Functional setState!

Going by Dan Abramov’s words in describing this pattern, Functional setState is a pattern where you

“Declare state changes separately from the component classes.”

Huh?

React 已经在 JavaScript 中普及了函数式编程。 这导致一些大型框架采用了 React 使用的基于组件的 UI 模式。 现在功能性发烧已经蔓延到整个网络开发生态系统中。

但 React 团队并没有停下脚步,他们继续深入挖掘,发现更多隐藏在神奇库中的强大函数。

所以今天我向你透露一个新函数,它是 React 中的黄金宝藏 —— setState!

它并不是全新的,只是内置于 React 的模式,难以被开发者发掘。

Dan Abramov 将 Functional setState 模式描述为:

与组件类分开声明状态更改。

Okay… what you already know

React is a component based UI library. A component is basically a function that accept some properties and return a UI element.

function User(props) {
  return (
    <div>A pretty user</div>
  );
}

A component might need to have and manage its state. In that case, you usually write the component as a class. Then you have its state live in the class constructor function:

class User {
  constructor () {
    this.state = {
      score : 0
    };
  }  render () {
    return (
      <div>This user scored {this.state.score}</div>
    );
  }
}

To manage the state, React provides a special method called setState(). You use it like this:

class User {
  ...   increaseScore () {
    this.setState({score : this.state.score + 1});
  }  ...
}

Note how setState() works. You pass it an object containing part(s) of the state you want to update. In other words, the object you pass would have keys corresponding to the keys in the component state, then setState()updates or sets the state by merging the object to the state. Thus, “set-State”.

基本概念

React 是一个基于组件的 UI 库。组件是一个接受一些属性并返回一个 UI 元素的函数。

function User(props){
  return(
    <div>漂亮的使用者</ div>
  );
}}

组件需要具有和管理其状态的能力。在这种情况下,通常将组件写为类。constructor 函数中就有了它的状态:

class User {
  constructor(){
    this.state = {
      分数:0
    };
  } render(){
    return(
      <div>此用户评分为{this.state.score} </ div>
    );
  }}
}}

为了管理状态,React 提供了一个名为 setState()的特殊方法:

class User {
  ... increaseScore(){
    this.setState({score:this.state.score + 1});
  } ...
}}

注意 setState()的工作方式。你传递一个对象,其中包含你要更新的状态的一部分。换句话说,传递的对象将具有与组件状态中的键相对应的键,然后 setState()通过将对象合并到状态来更新或设置状态。因此叫“状态设置(set-State)”。

What you probably didn’t know

Remember how we said setState() works? Well, what if I told you that instead of passing an object, you could pass a function?

Yes. setState() also accepts a function. The function accepts the previous state and current props of the component which it uses to calculate and return the next state. See it below:

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

Note that setState() is a function, and we are passing another function to it(functional programming… functional setState) . At first glance, this might seem ugly, too many steps just to set-state. Why will you ever want to do this?

你可能不知道的部分

还记得我们介绍的 setState() 的工作原理嘛? 除了其中的传递对象,我们还可以传递函数。

setState() 也接受一个函数作为参数。 该函数获取组件的 previous 状态和 current 属性,用于计算和返回下一个状态。 请看下面代码:

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

注意 setState() 是一个函数,我们将另一个函数传递给它作为参数(函数式编程...函数式 setState)。

初看这一操作似乎并不怎么样,调用 set-state 需要太多步骤了。那为什么要这样做呢?

Why pass a function to setState?

The thing is, state updates may be asynchronous.

Think about what happens when setState() is called. React will first merge the object you passed to setState() into the current state. Then it will start that reconciliation thing. It will create a new React Element tree (an object representation of your UI), diff the new tree against the old tree, figure out what has changed based on the object you passed to setState() , then finally update the DOM.

Whew! So much work! In fact, this is even an overly simplified summary. But trust in React!

React does not simply “set-state”.

Because of the amount of work involved, calling setState() might not immediately update your state.

React may batch multiple setState() calls into a single update for performance.

What does React mean by this?

First, “multiple setState() calls” could mean calling setState() inside a single function more than once, like this:

...
state = {score : 0};
// multiple setState() calls
increaseScoreBy3 () {
 this.setState({score : this.state.score + 1});
 this.setState({score : this.state.score + 1});
 this.setState({score : this.state.score + 1});
}...

为什么要给 setState 传递函数?

事情是这样子的,state 更新可能是异步的

想想当 setState() 被调用时发生了些什么。React 首先将你传递给 setState() 函数的对象合并到当前状态中。然后它将开始协调各部分。它将创建一个 React 元素树(用于表示 UI 的对象),对比新旧树的差别,并根据你传递给 setState() 函数对象的不同,计算出相应的变化部分,最终完成 DOM 更新。

工作量真大!事实上,这仅仅是一个简化版的总结。

React 并不只是简单的“set-state”。

因为包含大量的工作,调用 setState() 可能并不会立即更新你的 state。

React 可能会出于性能考虑将多个 setState() 调用合并成一个批处理更新操作。

这样做对 React 而言意味着什么呢?

首先,“多个 setState() 调用”可能意味着在一个单独的函数中调用 setState() 函数多于一次,如下代码:

...
state = {score : 0};
// multiple setState() calls
increaseScoreBy3 () {
 this.setState({score : this.state.score + 1});
 this.setState({score : this.state.score + 1});
 this.setState({score : this.state.score + 1});
}...

Now when React, encounters “multiple setState() calls”, instead of doing that “set-state” three whole times, React will avoid that huge amount of work I described above and smartly say to itself: “No! I’m not going to climb this mountain three times, carrying and updating some slice of state on every single trip. No, I’d rather get a container, pack all these slices together, and do this update just once.” And that, my friends, is batching!

Remember that what you pass to setState() is a plain object. Now, assume anytime React encounters “multiple setState() calls”, it does the batching thing by extracting all the objects passed to each setState() call, merges them together to form a single object, then uses that single object to do setState() .

In JavaScript merging objects might look something like this:

const singleObject = Object.assign(
  {}, 
  objectFromSetState1, 
  objectFromSetState2, 
  objectFromSetState3
);

This pattern is known as object composition.

In JavaScript, the way “merging” or composing objects works is: if the three objects have the same keys, the value of the key of the last object passed to Object.assign() wins. For example:

const me  = {name : "Justice"}, 
      you = {name : "Your name"},
      we  = Object.assign({}, me, you);we.name === "Your name"; //true
console.log(we); // {name : "Your name"}

Because you are the last object merged into we, the value of name in the you object?—?“Your name”?—?overrides the value of name in the me object. So “Your name” makes it into the we object… you win! :)

Thus, if you call setState() with an object multiple times?—?passing an object each time?—?React will merge. Or in other words, it will compose a new object out of the multiple objects we passed it. And if any of the objects contains the same key, the value of the key of the last object with same key is stored. Right?

现在 React 遇到“多次 setState() 调用”时, 不会真的三次完完整整地去“set-state"——它才不会像我上面说的那样去做如此庞大工作量的事情呢!它会对自己说:“不!我不想每次爬山时只带着一部分状态更新,然后去爬三次山。我更想要有一个容器,用来把这些状态一起打包全放在里面,然后一次性地把它们都带到山上去!”而这就是“批处理”(batching)。

我们传给 setState() 的是一个朴素的对象(plain object)。现在,假设任何时候当 React 遇到“多次 setState() 调用”时,它都会执行批处理,即提取所有单次传递给 setState() 的对象,把它们合并在一起形成一个新的单一的对象,并用这个单一的对象去做 setState() 的事情。

在 JavaScript 里面,合并对象可能会如同下面这种形式:

const singleObject = Object.assign(
  {}, 
  objectFromSetState1, 
  objectFromSetState2, 
  objectFromSetState3
);

上面这种模式就是我们所熟知的对象组合(object composition)。

JavaScript 中,合并(merging)或组合(composing)对象是这样工作的:如果三个对象有相同的 key, 传给 Object.assign() 的最后一个包含此 key 的对象会覆盖掉前面的值。例如:

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 值。 所以呢,you 赢啦!:)

你看,当你多次往 setState() 里传入对象调用此方法时,是每次传一个对象,React 会把这些对象合并。换句话说,它会基于我们传入的这多个对象来组合出一个新的对象。并且如果这多个对象有相同的 key, 最后一次传入的对象的 key 值会被储存下来。对吗?

That means that, given our increaseScoreBy3 function above, the final result of the function will just be 1 instead of 3, because React did not immediately update the state in the order we called setState() . But first, React composed all the objects together, which results to this: {score : this.state.score + 1} , then only did “set-state” once?—?with the newly composed object. Something like this: User.setState({score : this.state.score + 1}.

To be super clear, passing object to setState() is not the problem here. The real problem is passing object to setState() when you want to calculate the next state from the previous state. So stop doing this. It’s not safe!

Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

这意味着,以我们上面的 increaseScoreBy3 函数为例,函数的最终结果将只是 1 而不是 3,因为 React 没有立即按照我们调用 setState() 的顺序来更新 state。但是首先,React 将所有对象组合在一起,结果是:{score:this.state.score + 1},那么使用新组合的对象仅完成一次“set-state”。 像这样:User.setState({score:this.state.score + 1}。

更明确来说,将对象传递给 setState() 并不是问题所在。真正的问题在于当你想从前一个状态计算下一个状态时,传递给 setState() 的对象。这么做的风险大,理应停止。

因为 this.props 和 this.state 可能会异步更新,所以你不应该依赖它们的值来计算下一个状态。 

Functional setState to the rescue

If you’ve not spent time playing with the pen above, I strongly recommend that you do, as it will help you grasp the core concept of this post.

While you were playing with the pen above, you no doubt saw that functional setState fixed our problem. But how, exactly?

Updates will be queued and later executed in the order they were called.

So, when React encounters “multiple functional setState() calls” , instead of merging objects together, (of course there are no objects to merge) React queues the functions “in the order they were called.”

After that, React goes on updating the state by calling each functions in the “queue”, passing them the previous state?—?that is, the state as it was before the first functional setState() call (if it’s the first functional setState() currently executing) or the state with the latest update from the previous functional setState() call in the queue.

函数式的setState能拯救世界

Sophia Shoemaker 在 Pen 中演示了这个问题:

点击查看这一 Pen 能帮助你更好地了解这个问题。当你查看后你会发现 setState 解决了我们的问题。但是确切地说,是怎么解决的呢?

“状态更新”会被排列,然后按照它们被调用的顺序来执行。

所以,当 React 碰到“多次 setState() 调用”的情况时,它不会把对象合并在一起(当然了,这里并没有什么对象要被合并),它会按照调用的顺序把这些方法排个队。

接下来,React 依次调用队列中的方法,把上一个状态传递给当前的方法,从而不断更新状态。这里提到的“上一个状态”,分两种情况:

  • 对队列中第一个被执行的 setState() 而言,那就是在其被执行前的对象的本来状态 

  • 对队列中非第一个 setState() 而言,那就是队列里离它最近的 setState() 执行后生成的对象的状态。

Again, I think seeing some code would be great. This time though, we’re gonna fake everything. Know that this is not the real thing, but is instead just here to give you an idea of what React is doing.

Also, to make it less verbose, we’ll use ES6. You can always write the ES5 version later if you want.

First, let’s create a component class. Then, inside it, we’ll create a fake setState() method. Also, our component would have a increaseScoreBy3()method, which will do a multiple functional setState. Finally, we’ll instantiate the class, just as React would do.

class User{
  state = {score : 0};  //let's fake setState
  setState(state, callback) {
    this.state = Object.assign({}, this.state, state);
    if (callback) callback();
  }  // multiple functional setState call
  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();

Note that setState also accepts an optional second parameter?—?a callback function. If it’s present React calls it after updating the state.

Now when a user triggers increaseScoreBy3(), React queues up the multiple functional setState. We won’t fake that logic here, as our focus is on what actually makes functional setState safeBut you can think of the result of that “queuing” process to be an array of functions, like this:

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

我想直接拿代码来阐释。但这次只是模拟,目的是传达 React 的操作。

为避免冗杂,我将使用 ES6 语法。

首先,我们创建一个组件类。然后,在这个类里,我们创建一个 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();
  }  // multiple functional setState call
  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})
];

Finally, let’s fake the updating process:

// recursively update state in the order
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);

True, this is not as so sexy a code. I trust you could do better. But the key focus here is that every time React executes the functions from your functional setState, React updates your state by passing it a fresh copy of the updated state. That makes it possible for functional setState to set state based on the previous state.

Here I made a bin with the complete code. Tinker around it (possibly make it look sexier), just to get more sense of it.

最后,我们假设更新过程:

// recursively update state in the order
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 执行函数,并通过传递已更新状态的新副本来更新您的状态。 这使得功能 setState 可以基于先前状态设置状态。

这里我用完整的代码做了一个 bin。 Tinker 只为了使代码更完善。

返回顶部
顶部