[译] React 未来之函数式 setState

本文讲的是[译] 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 中的,因此 you 的 name 属性的值 “Your name” 会覆盖me 的 name 属性的值。因此 we 的 name 属性的值最终为 “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.props 和 this.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!






原文发布时间为:2017年3月20日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-09-10 10:30:40

[译] React 未来之函数式 setState的相关文章

[译] React 16 带来了什么以及对 Fiber 的解释

本文讲的是[译] React 16 带来了什么以及对 Fiber 的解释, 原文地址:What's New in React 16 and Fiber Explanation 原文作者:Trey Huffine 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:yoyoyohamapi 校对者:Tina92 sunui 特性概览 -- 万众期待的 React 16 React 核心算法的更新已经进行了多年了 -- 这次更新提供了一个从底层重写了 Reac

[译] React 在服务端渲染的实现

本文讲的是[译] React 在服务端渲染的实现, 原文地址:Server-Side React Rendering 原文作者:Roger Jin 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:牧云云 校对者:CACppuccino.xx1124961758 React 在服务端渲染的实现 React是最受欢迎的客户端 JavaScript 框架,但你知道吗(或许更应该试试),你可以使用 React 在服务器端进行渲染? 假设你为客户构建了一个很棒的

【译】Object Dumper: 函数式程序设计编码中的强大工具

Post in English:Object Dumper: An Invaluable Tool for Writing Code in the Functional Programming Style   当用函数式程序设计的方式开发C#应用程序时,你经常需要把一个集合输出到控制台.Object dumper在这方面是一个强大的工具.本为中的例子只有很少开发者了解. 功能的转换通常需要连续的变换:转换集合a=>集合b=>集合c=>最终的集合.这是我的Functional Progra

[译]预测未来的公司有何过人之处?

如果知识就是力量,那么预测分析便应诺了关于未来的终极认知. 这样的认知来的并不容易,但是数字信息的日益密集化.公司之间更深层次的自动连接以及增强的存储和计算能力为企业家们创造了新的选项.历史上首次可预测的未来已经触手可及,这其中包括对潜在的未来行为和结果的各种可能性以及日益增长的认知.这也就难怪,根据埃森哲最近的一项调查表明,从2012年开始,高管们将预测分析放置在执行议程的首位. 各种机构正在经营新型关系,以更多地了解潜在的未来行为.结果及其发生概率,并采取相应的行动.我们发现,最具前瞻性的企

[译] setState() 门事件

本文讲的是[译] setState() 门事件, 原文地址:setState() Gate 原文作者:Eric Elliott 译文出自:掘金翻译计划 译者:reid3290 校对者:1992chenlu,qinfanpeng React setState() 解惑 译注:本文起因于作者的一条推特,他认为应该避免使用 setState(),随后引发论战,遂写此文详细阐明其观点.译者个人认为,本文主要在于"撕逼",并未深入介绍 setState() 的技术细节,希望从技术层面深入了解 s

[译]Angular vs React:谁更适合前端开发

本文讲的是[译]Angular vs React:谁更适合前端开发, 原文地址:Angular vs. React: Which Is Better for Web Development? 原文作者:Brandon Morelli 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:龙骑将杨影枫 校对者:Larry.薛定谔的猫.逆寒 大家总在写文章争论,Angular 与 React 哪一个才是前端开发的更好选择(译者:在中国还要加上 vue :P).我

[译] Redux 有多棒?

本文讲的是[译] Redux 有多棒?, 原文地址:What's So Great About Redux? 原文作者:Justin Falcone 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO/whats-so-great-about-redux.md 译者:ZiXYu 校对者:MJingv, calpa Redux 有多棒? Redux 能够优雅地处理复杂且难以被 React 组件描述的状态交互.

React 还是 Vue:你该如何选择?

2016年React巩固了它作为前端框架之王的地位,这一年中可以看到它在Web端和移动端的快速成长,同时稳稳领先于它的主要竞争对手Angular. 但是2016对Vue来说也是同样令人印象深刻的一年,它发布了Vue 2.0版本并且在JavaScript社区引起了巨大反响,GitHub上多出的25000颗star就是最好的证明. React和Vue的适用范围无疑是很相似的:同样是基于组件的轻量级框架,同样专注于用户界面的视图层.同样可以用在简单的项目中,也同样可以使用全家桶扩展为复杂的应用程序.

react router-React Router 2.0 使用不成功,求大神解救

问题描述 React Router 2.0 使用不成功,求大神解救 最近学使用React和React的路由,但是第一个DEMO一直写不出来,现在报错是 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>react</title> <script src="libs/react-0.14.8/build/react-with-add