[译] React 未来之函数式 setState

React 使得函数式编程在 JavaScript 领域流行了起来,这驱使大量框架采用 React 所推崇的基于组件的编程模式,函数式编程热正在大范围涌向 web 开发领域。

但是 React 团队却还不“消停”,他们持续深耕,从 React(已经超神了!)中发掘出更多函数式编程的宝藏。

因此本文将展示深藏在 React 中的又一函数式“宝藏” —— 函数式(functional)setState

好吧,名字其实是我乱编的,而且这个技术也称不上是新事物或者是个秘密。这一模式内建于 React 中,但是只有少数 React 深耕者才知道,而且从未有过正式名称 —— 不过现在它有了,那就是函数式 setState

正如 Dan Abramov 所言,在函数式 setState 模式中,“组件 state 变化的声明可以和组件类本身独立开来”。

这?

你已经知道的是...

React 是一个基于组件的 UI 库,组件基本上可以看作是一个接受某些属性然后返回 UI 元素的函数。

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

组件可能需要持有并管理其 state。在这种情况下,一般将组件编写为一个类,然后在该类的 constructor 函数中初始化 state:

class User {
  constructor () {
  this.state = {
      score : 0
    };
  }

  render () {
    return (
      <div>This user scored **{this.state.score}**</div>
    );
  }
}复制代码

React 提供了一个用于管理 state 的特殊函数 —— setState(),其用法如下:

class User {
  ...

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

  ...
}复制代码

注意 setState() 的作用机制:你传递给它一个对象,该对象含有 state 中你想要更新的部分。换句话说,该对象的键(keys)和组件 state 中的键相对应,然后 setState() 通过将该对象合并到 state 中来更新(或者说 sets)state。因此称为 “set-State”。

你可能还不知道的是...

记住 setState() 的作用机制了吗?如果我告诉你说,setState() 不仅能接受一个对象,还能接受一个函数作为参数呢?

没错,setState() 确实可以接受一个函数作为参数。该函数接受该组件前一刻的 state 以及当前的 props 作为参数,计算和返回下一刻的 state。如下所示:

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

注意 setState() 本身是一个函数,而且我们传递了另一个函数给它作为参数(函数式编程,函数式 setState)。乍一看可能觉得这样写挺丑陋的,set-state 需要的步骤太多了。那为什么还要这样写呢?

为什么传递一个函数给 setState?

理由是,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 中的,因此 youname 属性的值 “Your name” 会覆盖 mename 属性的值。因此 wename 属性的值最终为 “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.propsthis.state 可能是异步更新的,你不能依赖这些值计算下一个 state。

下面 Sophia Shoemaker 写的一个例子展示了上述问题,细细把玩一番吧,留意其中好坏两种解决方案。

代码链接

让函数式 setState 来拯救你

如果你还未曾把玩上面的例子,我还是强烈建议你玩一玩,因为这有利于你理解本文的核心概念。

在把玩上述例子的时候,你肯定注意到了 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 称之为“宝藏”。

React 最为深藏不露的秘密

至此,我们已经深入探讨了为什么多次函数式 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 未来式

最近几年,React 团队一直都致力于更好地实现 stateful functions

函数式 setState 看起来就是这个问题的正确答案(也许吧)。

Hey, Dan!还有什么最后要说的吗?

如果你阅读至此,估计就会和我一样兴奋了。即刻开始体验函数式 setState 吧!

欢迎扩散,欢迎吐槽(Twitter)。

Happy Coding!

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值