Week9 - RxJS到底帮助了我们什么,用简单的实战来说明 - Reactive Programing篇 [前端大作

各位好,不知道各位是否有听过Functional Programming - FP,这是近期很火红的名词。

我第一次听到这个名词是一个前辈说的:「FP实在太神奇了,可恶,太晚知道FP了,真是千金难买早知道啊」,由于前辈实在很厉害,所以我当初对于FP的感受彷彿白金之星。

「我的code要飞起了吗?!」,抱着这样的心态我进一步请教他,于是他贴给了我FP的一大重点 - 「範畴学」,我一点开连结就

...

这是我当下的感受:

神人的世界就是这么朴实无华且枯燥。

FP的浅见

我个人偏向于实务主义,我常常会想FP到底实际帮助了我们什么,但许多FP文章都在探讨FP的核心,例如纯函数、惰性求值、Monad等等,这些都很酷,但换到实战中,就是不断不断的卡关,举个纯函数例子:

有个addOne的函数会对任何数字加一,即输入1的到的结果必定为2,那如果现在需要有getNumber函数去DB拿数字1,再丢去addOne函数执行,

我们要怎么确定getNumber函数去DB拿数字1是铁定拿得到的呢?

答案是不可能的,在程式的世界中,铁定有不纯的地方,我们没办法保证所有东西都纯,假设DB没有数字1,getNumber还回传1给我,那DB根本没有存在意义,FP所说的纯函数,主要是希望使用者更清楚地分开纯与不纯的界线,不让整个程式很混乱,更详细的介绍可以看良葛格的「解开对函数式的误解」

可是知道这件事后,并没有让我有得到白金之心的感觉,就是那种「恩!这的确是很正确的思想」,可是前辈所说的很神奇到底是神奇在哪@@?!

直到看到RxJS

就在有次,我遇到了一个以下的情境:

按A按钮要打一个A API,回传资料后做alert A按B按钮要打一个B API,回传资料后做alert B并且在A、B按钮都按完后,透过A与B API的资料做alert C

我们可以模拟成以下情境:

按A按钮要取得按A的滑鼠位置,回传资料后做alert A按B按钮要取得按B的滑鼠位置,回传资料后做alert B并且在A、B按钮都按完后,透过A与B的滑鼠资料做alert C

我们先来展示传统的作法:

线上运行jsfiddle

function alertC () {  if (Object.keys(mouseDatas.A).length === 0 || Object.keys(mouseDatas.B).length === 0) return  window.alert(`Alert C. A button x is ${mouseDatas.A.screenX}. B button x is ${mouseDatas.B.screenX}`)  mouseDatas.A = {} //Reset A  mouseDatas.B = {} //Reset B}const mouseDatas = {  A: {},  B: {},}document.getElementById('A').addEventListener('click', mouseData => {  mouseDatas.A = mouseData  window.alert('Alert A')  alertC()})document.getElementById('B').addEventListener('click', mouseData => {  mouseDatas.B = mouseData  window.alert('Alert B')  alertC()})

由于A按钮与B按钮之间存在着「相依」的关係,所以我们必须生出一个global变数mouseDatas来「分享」彼此的资料

这样会导致一些问题:

这是我认为最重要的点,就是第二行的判断式子
if (Object.keys(mouseDatas.A).length === 0 || Object.keys(mouseDatas.B).length === 0)
我们需要透过这个判断来确定alertC()在执行的「时间点」mouseDatas是否我们所期望的状态,就是mouseDatas的A与B都有值mouseDatas需要reset A与B,如果错的「时间点」reset或者就会导致整个流程出错整体流程的意图有些混乱,在A click的「时间点」会触发alertC函数吗?并不一定,因为他受到B click的影响。alert()这函数其实不完全属于A click或者B click,他是属于两者都被点击的状况,可读性上面我们是容易被误导的我们扩大一下情境,如果新增D~Z这些按钮,并全部按完要alertC函数,那我们全部的按钮都要添加alert()这段code

大家有发现我一直强调「时间点」吗?因为我个人认为RxJS最大解决的问题就是

他明确的表达什么时间点该做什么

我们再来看看RxJS的作法:

线上运行jsfiddle

const { zip, fromEvent } = rxjs;document.getElementById('A').addEventListener('click', () => {    window.alert('Alert A')})document.getElementById('B').addEventListener('click', () => {    window.alert('Alert B')})// alertC的逻辑zip(    fromEvent(document.getElementById('A'), 'click'),    fromEvent(document.getElementById('B'), 'click')).subscribe(mouseDatas => {    window.alert(`Alert C. A button x is ${mouseDatas[0].screenX}. B button x is ${mouseDatas[1].screenX}`)})

整体程式码的意思是这样的,我不用太複杂的一些专有名词来解释,而用一些口语的方式:

zip: 可以将两个event做合併fromEvent: 取得这按钮被click的eventsubscribe: 上面两个event被触发后,会执行的函数,并且也会取得这两个event的资料,会以array传入,这边以mouseDatas命名

与传统的作法做比较:

我们不需要判断时间的式子了,因为zip到subscribe这几行的意思就是「我要在两个event都执行完的时间点执行alertC」因为没有global变数,我们不需要reset,subscribe时拿到的资料是很纯粹的mouseDatas,他几乎没有可能被其他人修改,不用很害怕他是不是原本的资料还是被改过整体alertC的逻辑可读性是很好的,因为他没有穿插在A click或是B click之中,他就是一个「从zip到subscribe这几行可以表达的一个逻辑」就算我们扩大情境成有D~Z个按钮,也是单纯在zip里面添加event,不需要去修改各个按钮的click逻辑

稍微统整,并与FP观念融合一下

传统做法上我们透过一个global变数来达到「判断不同时间点」的能力,但RxJS时直接透过「一行一行来表达什么时间点该做什么事」。而这一行一行的作法我们称为「流」

在对于时间的表达上有根本的不同,另外,传统做法上我们也可以依靠callback里面再塞callback来达到「有顺序」的执行,这跟时间也是有关係,但RxJS可以靠一个简单的concat来解决,连结里面的code大致如下

import { of, concat } from 'rxjs';const sourceOne = of(1, 2, 3);const sourceTwo = of(4, 5, 6);const example = concat(sourceOne, sourceTwo);const subscribe = example.subscribe(val => console.log(val));

而意思口语上就是

两者不同时间点执行但我会有顺序处理资料(event1, evnet2).处理方法subscribe(val => console.log(val))

所以我们才说RxJS提供了新思维,

RxJS提供了流的方法来处理不同时间点的资料,让我们不用透过改global变数或者callback里面叫callback这样不好「表达时间意图」的方式来处理

所以与FP观念做融合就是,我这边特别列出我自己以前的误解xd:

纯函数: 我们不採用流程外的变数来表达整个流程的目的。但不太表整个流程没有副作用,click传回来的mouseData就是副作用声明式编程很好表达流程: 我们只需透过RxJS的各个函数组合来组合click event,并把流程一行一行描述出来,这个我们称为声明式编程,而不用在A click与B click添加alert()逻辑,使得逻辑散乱,这是指令式编程的一种做法。但不代表声明式的表达能力好于指令式,比如说以下
function addOne(number) {  return number + 1}function getEven(number) {  return number % 2 === 0}let finalNumber = 0//声明式finalNumber = [1, 2, 3, 4]  .map(addOne)  .filter(getEven)  .reduce((accumulator, currentValue) => accumulator + currentValue)console.log(finalNumber)//指令式finalNumber = 0;[1, 2, 3, 4].forEach(number => {  const addOneNumber = addOne(number)  if (getEven(addOneNumber)) finalNumber = finalNumber + addOneNumber})console.log(finalNumber)
以这样的例子其实我们看不出声明式有更好表达流程,RxJS主要是透过FP的组合特性来组合各种event,所以自然而然就会变成声明式的作法,好读也是因为event变成了流的特性,但我们不能一竿子打翻一条船的说声明式一定讚惰性求值与Monad: 我们这些整个流程最后执行的方式,当然要等到event来在执行,因此惰性求值与Monad就是为了达到此目的,如果没有此想法会觉得请函数晚点执行会很不解。

最后分享一下后端也是有使用RxJS情境的

RxJS通常在前端讨论,而后端较少,最根本的原因是因为后端通常就是一个requests一个response,我们较少遇到一个requets之中会有不同「时间点」的event,但我们还是有机会遇到的,

比如说: Promise要retry。我相信这是promise爱用者的一个大痛点,用传统的方法通常採用递迴,但逻辑流程会很不清楚,RxJS的作法如下:

线上运行runkit

const { from, of } = rxjs;const { switchMap, retry } = rxjs.operators;function getSearchResults(url) {  return new Promise((resolve, reject) => {    console.log('do again')    reject("Reject")  })}of("http://foo.com").pipe(  switchMap(url => from(getSearchResults(url))),  retry(3)).subscribe({  next: val => console.log(val),  error: val => console.log(`Get error ${val}`)})

对于RxJS,我也是个新手,坦白说我主要以后端为主,所以使用到RxJS的情境没有前端那么直观,在与许多前辈与六角学院的高人讨论之后才渐渐稍有RxJS的「流式思维」,不然,我其实在FP的範畴论、Monad、functor里面打转,当然这是很重要的,但要怎么应用我以前一直不太确定。

也希望大家可以分享自己对于RxJS的想法,也欢迎指正我的文章,一起来分享这些新思考方式吧!

最近看JoJo看到完全是中毒状态

参考资料

FRP与函数式
RxJava 沉思录
希望是最浅显易懂的 RxJS 教学


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章