• 原文地址:How to make your React app fully functional, fully reactive, and able to handle all those crazy side effects
  • 原文作者:Luca Matteis
  • 译文出自:掘金翻译计划
  • 译者:ZhangFe
  • 校对者:AceLeeWinnie,liucaihua9

如何让你的 React 应用完全的函数式,响应式,并且能处理所有令人发狂的副作用

函数响应式编程 (FRP) 是一个在最近获得了无数关注的编程范式,尤其是在 JavaScript 前端领域。它是一个有很多含义的术语,却描述了一个极为简单的想法:

所有的事物都应该是纯粹的以便于测试和推理 (函数式),并且使用随时变化的值给异步行为建模 (响应式)

React 本身并非完全的函数式,也不是完全的响应式。但是它受到了一些来自 FRP 背后理念的启发。例如 函数式组件 就是一些依赖他们 props 的纯函数。 并且 他们响应了 prop 和 state 的变化.
(译者注:无状态组件只接收 props ,这里的 state 应该是指父元素的)

但是一谈到副作用的处理(side effects),仅作为视图层的 React 就需要一些其他库的帮助了,比如说Redux。

在这篇文章里我会谈谈 redux-cycles,它是一个 Redux 中间件,借助 Cycle.js 框架的优势,帮助你以一种函数式和响应式的方法处理你 React 应用中的副作用和异步代码,这是一个尚未被其他 Redux 副作用模型共享的特征。

什么是副作用?

副作用即是改变了外部世界的行为。你的应用里所有发出 HTTP 请求,写入 localStorage 的操作,或者甚至操作 DOM 都被认为是副作用。

副作用是不好的,他们很难去测试,维护起来很复杂,并且通常你的 bug 都出现在这里。因此你的目标就是最小化或者定位他们。

“由于有副作用的存在,一个程序的行为依赖于历史记录,即代码执行的顺序,因为理解一个有效的程序需要考虑到所有可能的历史记录,副作用经常会使一个程序很难理解。” — Norman Ramsey

以下是几种现今用来处理 Redux 中的副作用比较流行的方法:

  1. redux-thunk — 将你有副作用的代码放在 action creators 中
  2. redux-saga — 使用 saga 声明你的副作用逻辑
  3. redux-observable — 使用响应式编程来给副作用建模

然而问题是以上方法中没有一个既是纯函数式的又是响应式的。他们中有的(redux-saga)是纯函数有些(redux-observable)则是响应式的,但是没有一个拥有我们前文介绍的 FRP 所拥有的所有的概念。

Redux-cycles 既是纯函数又是响应式的

首先我们会更详细地解释这些函数式和响应式的概念以及为什么你需要关心这些,然后会详细介绍 redux-cycles 是如何工作的。


使用 Cycle.js 以纯函数的方式处理副作用

HTTP 请求大概是最常见的副作用了。下面是一个使用 redux-thunk 发出 HTTP 请求的例子:

function fetchUser(user) {return (dispatch, getState) => fetch(`https://api.github.com/users/${user}`)
}复制代码

这个函数是命令式的。虽然它返回了一个 promise 并且你可以使用其他 promises 来链式调用它,但是 fetch() 已经执行了,在这个特定时刻它已经不是一个纯函数了。

这同样适用于 redux-observable:

const fetchUserEpic = action$ =>action$.ofType(FETCH_USER).mergeMap(action =>ajax.getJSON(`https://api.github.com/users/${action.payload}`).map(fetchUserFulfilled));复制代码

ajax.getJSON() 使得这段代码是命令式的。

为了保证一个 HTTP 请求是纯粹的,你不应该去想“立刻发送一个 HTTP 请求”而是应该“描述一下我希望 HTTP 请求是什么样的”并且不要担心它何时发出去或者谁调用了它

这就是你在 Cycle.js 中编写所有代码的本质。你使用这个框架所做的每件事都是创建你想做某事的描述。这些描述之后会被发送给那些实际关心 HTTP 请求的 drivers (通过响应式数据流)。

function main(sources) {const request$ = xs.of({url: `https://api.github.com/users/foo`,});return {HTTP: request$};
}复制代码

就像你在上面这个代码片段中看到的,我们并没有调用函数去发出请求。如果你执行这段代码你会发现请求立即就发出了,那么背后究竟发生了什么呢?

神奇之处就在于 drivers。当你的函数返回了一个包含 HTTP 键值的对象时,Cycle.js 知道需要处理它从数据流收到的消息,并且执行相应的 HTTP 请求(通过 HTTP driver)。

关键的一点是,你虽然没有摆脱副作用,HTTP 请求依然要发出,但是你将它定位在了你的应用代码之外

你的函数更加容易理解,尤其是更容易测试,因为你只要测试你的函数是否发出了正确的消息,不需要浪费那些无用的 mock 时间。

响应式副作用

在之前的例子里我们提到了响应式。这需要有一种和这些 drivers 沟通“在外部世界做某事”和被告知“外部世界有某事已经发生了”的方式。

Observables (aka streams) 是对于这类异步交互的完美抽象。

每当你想“做某事”时,你会向输出流发出你想做什么的描述。在 Cycle.js 里这些输出流被称作 sinks

每当你想“被通知某事”你只要使用一个输入流(被称作sources)并且遍历一次流的值就能知道发生了什么。

这形成一种 反应式 循环,相比于一般的命令式代码,你需要一个不同的思维来理解它。
让我们使用这个范例来建模一个HTTP请求/响应生命周期:

function main(sources) {const response$ = sources.HTTP.select('foo').flatten().map(response => response);const request$ = xs.of({url: `https://api.github.com/users/foo`,category: 'foo',});const sinks = {HTTP: request$};return sinks;
}复制代码

HTTP driver 知道这个函数返回的 HTTP 键值。这是一个包含请求 GitHub 链接的 HTTP 请求流描述。它正在告诉 HTTP driver :“我想要请求这个地址”。

之后这个 dirver 知道要执行请求,并且将返回值作为 sources(sources.HTTP)返回给 main 函数 — 注意 sinks 和 sources 使用相同的键值。

让我们再解释一次:我们用 sources.HTTP“被通知 HTTP 已经返回了”,并且我们返回了sinks.HTTP 来“发送 HTTP请求”

这里有一个动画来解释这一重要的响应式循环:

相比于一般的命令式编程,这似乎是反直觉的:为什么读取响应值的代码在发出请求的代码之前?

这是因为在 FRP 中代码在哪是不重要的。所有你要做的就是发送描述,并且监听变化,代码的顺序并不重要。

这使得代码非常容易重构。


介绍 redux-cycles

此时你可能会问,所有的这些和我的 React 应用有什么关系?

仅仅通过写一些你想做某事的描述,你已经学习到了使用纯函数的优势,并且学习了用观察者去和外部世界交流的优势。

现在,你将看到如何在你当前的 React 应用里使用这些概念去变成完全的函数式和响应式。

拦截并且调度 Redux 行为

使用 Redux 时你需要 dispatch actions 来告诉你的 reducers 你需要一个新的state。

这是一个同步的流程,意味着一旦你想执行异步行为(为了副作用)你需要使用一些中间件来拦截这些 actions,相应的,你要触发其他的 actions 来执行这个异步副作用。

这正是 redux-cycles 所做的。它是一个中间件,拦截了 redux actions 后进入 Cycle.js 的响应式循环,并且允许你使用 drivers 去执行其他副作用。然后它基于你函数里的异步数据流描述 dispatch 一个新的 action。

function main(sources) {const request$ = sources.ACTION.filter(action => action.type === FETCH_USER).map(action => ({url: `https://api.github.com/users/${action.payload}`,category: 'users',}));const action$ = sources.HTTP.select('users').flatten().map(fetchUserFulfilled);const sinks = {ACTION: action$,HTTP: request$};return sinks;
}复制代码

在上面这个例子里有一个新的 source 和 sink - ACTION。但是数据通信的模式是一致的。

它使用 sources.ACTION 来监听被 Redux 调用的 actions。并且通过返回 sinks.ACTION 来dispatch 新的 actions。

具体点说它是触发了标准的 Flux Actions objects。

最酷的事情是你可以结合其他 drivers 发生的事。在之前的例子里 HTTP 域里发生的事确实触发了 ACTION 域,反之亦然

— 注意,与 Redux 的通信完全通过 ACTION 的 source 和 sink。Redux-cycle 的 drivers 负责处理实际的 dispatch。

更复杂的应用程序?

如果只写那些转换数据流的纯函数该如何开发一个复杂的应用呢?

使用已有的 drivers你已经可以做很多事了。或者你可以创建你自己的 drivers — 下面是一个简单的 driver,它在控制台上输出了写入其 sink 的消息。

run(main, {LOG: msg$ => msg$.addListener({next: msg => console.log(msg)})
});复制代码

run 是 Cycle.js 的一部分,它执行你的 main 函数(第一个参数)并且传入其他所有的 drivers(第二个参数)。

Redux-cycles 推荐了两个你可以和 Redux 通信的 drivers, makeActionDriver() & makeStateDriver():

import { createCycleMiddleware } from 'redux-cycles';const cycleMiddleware = createCycleMiddleware();
const { makeActionDriver, makeStateDriver } = cycleMiddleware;const store = createStore(rootReducer,applyMiddleware(cycleMiddleware)
);run(main, {ACTION: makeActionDriver(),STATE: makeStateDriver()
})复制代码

makeStateDriver() 是一个只读的 driver。这意味着在你的 main 函数里只能读取sources.STATE。你不能让它做什么,只能从它读取数据。

每当 Redux 的 state 发生了变化,sources.STATE 流就会触发产生一个新的 state 对象。当你需要基于当前应用的数据写一些特定逻辑时 非常有用

复杂的异步数据流

响应式编程的另一个巨大优势就是能够使用运算符将流组成其他流,可以随时将它们当做数据对待:你可以对它们进行 map filter 甚至 reduce 这些操作。

运算符使得显式的数据流图(即操作符之间的依赖逻辑)成为可能。允许你通过各种操作符将数据流可视化,就像上面的动画一样。

Redux-observable 也允许你写复杂的异步流,他们用一个复杂的 WebSocket 例子作为它们的卖点,然而以纯函数的方式编写这些流才是 Cycle.js 真正区别于其他方式的强大之处。

由于一切都是纯数据流,我们可以想象到未来的编程将只是将操作符块连接到一起。

使用弹子图(marble diagrams)测试

最后但也值得关注的是测试。这才是 redux-cycles(和通常所有的 Cycle.js 应用一样)真正闪耀的地方。

因为你的应用代码里都是纯函数,要测试你的主要功能,你只需要将其作为输入流,并将特定流作为输出即可。

使用这个很棒的 @cycle/time 项目,你甚至可以画一个 弹子图 并且以一种可视化的方式去测试你的函数:

assertSourcesSinks({ACTION: { '-a-b-c----|': actionSource },HTTP:   { '---r------|': httpSource },
}, {HTTP:   { '---------r|': httpSink },ACTION: { '---a------|': actionSink },
}, searchUsers, done);复制代码

这段代码 执行了 searchUsers 函数,将特定源作为输入(以第一个参数的方式)。给定的这些 sources 期望函数返回所提供的 sinks(以第二个参数的方式)。如果不是,断言就会失败。

当你需要测试异步行为时,以图形的方式定义流特别有用。

HTTP 源发出一个 r (响应),你会立刻看到 a(action)出现在 ACTION sink 中 — 他们同时发生。然而,当 ACTION source 发出一段 -a-b-c,你不要指望此时 HTTP sink 会发生什么。

这是因为 searchUsers 去抖了他接收到的 actions。它只会在 ACTION source 流停止活动 800 毫秒后发送 HTTP 请求,这是一个自动完成的功能。

测试这种异步行为对于纯函数和响应式函数来说是微不足道的。

结论

在这篇文章里我们介绍了 FRP 的真正力量。我们介绍了 Cycle.js 和它新颖的范式。如果你想学习更多的关于 FRP 的知识,Cycle.js awesome list 是一个很重要的资源。

只使用 Cycle.js 本身而不使用 React 或者 Redux 可能有点痛苦, 但是如果你愿意放弃一些来自 React 或 Redux 社区的技术和资源的话还是可以做到的。

另一方面,Redux-cycles 允许你继续使用所有的伟大的 React 的内容并且使用 FRP 和 Cycles.js 使你更加轻松。

也十分感谢 Gosha Arinich 以及 Nick Balestra 和我一起维护这个项目,也谢谢 Nick Johnstone 校对这篇文章。

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

[译] 如何让你的 React 应用完全的函数式,响应式,并且能处理所有令人发狂的副作用...相关推荐

  1. [译] 项目什么时候需要 React 框架呢?

    本文讲的是[译] 项目什么时候需要 React 框架呢?, 原文地址:When Does a Project Need React? 原文作者:CHRIS COYIER 译文出自:掘金翻译计划 译者: ...

  2. [译]函数式响应编程入门指南

    原文地址:An Introduction to Functional Reactive Programming 原文作者:Daniel Lew 译文出自:掘金翻译计划 本文永久链接:github.co ...

  3. [译] 响应式 Web 应用(一)

    本文由 Shaw 发表在 ScalaCool 团队博客. 原书 www.manning.com/books/react- 第一章:你是说「响应式」? 本章内容 响应式应用及其起源 为什么响应式应用是必 ...

  4. [react] 类组件和函数式组件有什么区别?

    [react] 类组件和函数式组件有什么区别? 函数式组件没有state和一系列的钩子函数,只接收一个props 个人简介 我是歌谣,欢迎和大家一起交流前后端知识.放弃很容易, 但坚持一定很酷.欢迎大 ...

  5. [react] 什么是React的实例?函数式组件有没有实例?

    [react] 什么是React的实例?函数式组件有没有实例? React的实例:通过继承React.Component的类生成 函数式组件没有实例 个人简介 我是歌谣,欢迎和大家一起交流前后端知识. ...

  6. [译]使用MVI打造响应式APP(三):状态折叠器

    原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART3 - STATE REDUCER 作者:Hannes Dorfmann 译者:却把清梅嗅 在上一章节中,我 ...

  7. React实例练习-响应式设计、数据绑定、列表渲染、删除单项

    服务菜单 最好的学习就是在实战中成长,做一个<小姐姐服务菜单>应用,练习前面的知识和学习新知识 新建小姐姐组件 先在SRC的目录下面,新建一个文件Xiaojiejie.js 然后写一个基本 ...

  8. [译]使用MVI打造响应式APP(八):导航

    原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART 8 - NAVIGATION 作者:Hannes Dorfmann 译者:却把清梅嗅 在上一篇博客中,我们 ...

  9. 从零开始学习React——(六):React响应式设计和数据绑定

    本节主要介绍React中的响应式设计和数据绑定的方法. jQuery和React的区别 jQuery以事件驱动,原理是通过事件的触发来操作DOM改变页面. React 以数据驱动,为单向数据流,通过监 ...

  10. [译] 响应式脑电波—如何使用 RxJS、Angular、Web 蓝牙以及脑电波头戴设备来让我们的大脑做一些更酷的事...

    原文链接: medium.com/@urish/reac- 本文为 RxJS 中文社区 翻译文章,如需转载,请注明出处,谢谢合作! 如果你也想和我们一起,翻译更多优质的 RxJS 文章以奉献给大家,请 ...

最新文章

  1. 稳定性三十六计-幂等设计
  2. 沙雕记(1) 之 Land Grab
  3. ThinkPHP公共配置文件与各自项目中配置文件组合的方法
  4. JNI教程与技术手册
  5. GYM101002C - Greetings!
  6. b2b优势与劣势_在Amazon Business平台销售的优点和缺点,B2B卖家清楚吗?
  7. Ajax跨域请求与解决方案
  8. pytorch —— transforms图像增强(一)
  9. Mybatis的动态拼接条件
  10. 甜蜜暴击,情人节插画素材,甜而不腻!
  11. web前端要学哪些东西,前端大牛分享的技能整理
  12. proteus数码管不亮是什么原因_人行道闸开后不关的原因是什么?速来get一下
  13. css span 右端对齐_使用 CSS 实现具有方面感知的幽灵按钮
  14. linux分区命令mtd,修改IPQ4019/4018的MTD分区
  15. C语言数据结构课程设计(可运行)
  16. 电脑一复制粘贴就卡死_CAD复制粘贴时卡死的解决方法步骤
  17. 前端布局篇之文字居中显示
  18. 淘宝与拍拍的世纪之战!(庄帅)
  19. java实现注册的短信验证码
  20. UDA/语义分割-ColorMapGAN: Unsupervised Domain Adaptation for Semantic Segmentation Using Color Mapping G

热门文章

  1. MongoDB 分片操作
  2. [JZOJ4640] 【GDOI2017模拟7.15】妖怪
  3. css中hack是什么
  4. 微信小程序学习笔记(阶段一)
  5. 实验一: 网络侦查与网络扫描
  6. [bzoj3668][Noi2014]起床困难综合症/[洛谷3613]睡觉困难综合症
  7. .msi文件安装出现2503、2502错误
  8. 韩顺平循序渐进学java 第18讲 查找
  9. mahout推荐15-在hadoop上运行MapReduce
  10. 《Unix环境高级编程》学习笔记