接触Rxjs已经有一些时日了,只是一直没能真正实践过。最近工作上有个功能让我不得不对Rxjs进行更多深入的思考。其实Rxjs这个玩意儿从理论上就已经很难理解了,但是在实践中会发现更加棘手。
案例是这样的(见下图):有个report页,它对应着若干个macro(姑且先不管report和macro的业务含义,只需要知道report和macro是一对多的关系)。然后有两组操作:edit和delete 。系统针对用户对于不同的macro有不同的操作权限,需要根据在列表中选择的macro结合它们的权限来控制上述两组操作按钮是否可用,其中的业务逻辑如下:
- edit:有且仅有一个macro的时候,并且这个macro有edit的权限才可用
- delete:至少有一个macro的时候,并且所有的macro都有delete的权限才可用
图片描述
大致的思路很简单,我们需要获取每个selected macros的权限,然后根据”edit“和”delete“各自不同的逻辑去组合相应的权限,然后得出它们是否可用的逻辑值。这里有三个前提要注意:
- Selected macros是一个observable的流,当选项改变时它会发射当前已选的macro的集合。
- 每个macro的权限也是一个observable的流,因为权限是通过发送API请求去获取的,所以是一个异步操作,返回的应该是一个包含权限的流。
- 我们按钮得到的是否可用的逻辑值也需要包装在一个observable流里面。
所以问题来了,如果这只是单纯的同步操作,基本上只需要简单的逻辑运算就OK了,然而我们的场景是需要在流上进行逻辑运算的,并且最终结果还是流,这就是本文需要讲的主题。
我们先来说delete,回顾一下delete的业务逻辑:至少有一个macro的时候,并且所有的macro都有delete的权限才可用。我们大致有这样的思路:
if (checkedItems.length > 0) { return true; } else { foreach(checkedItem of checkedItems) { permission = getPermissionFromAPI(checkedItem.Id); } do "&" with all permission; return result; }
我们获取的每一个macro权限都是一个流,得到了一个流的集合,然后还要把集合中每一个流里面的数据取出来进行&运算。每个权限都独自成流,然而我还要对这若干的流进行组合操作,很容易想到的就是mergy为一个流,我一开始的算法是这样的:把所有的权限流放在一个数组里,然后用Observable.from(arr)把它变成“流的流”,也就是发射的数据本身也是个流的流,然后用mergeAll把这个双重流中的数据合并到一起变成单个流。然后对单个的流取出权限,进行&操作。难题又来了:怎么把这些delete权限进行&运算得到一个是否可用的boolean值。我们是进行流操作的,当然不可能像同步式编程那样取出所有权限值进行链式的&(p1 & p2 & p3),那么什么样的操作符可以帮助我们完成类似的效果呢?其实我们发现这种链式的&操作其实就是一种“归并操作”,我们平日里总是以求和来举例“归并reduce”,其实这种逻辑的链式操作又何尝不是一种reduce呢。reduce的初始值为selecteditems.length>0,然后取出合并流中每个delete权限和初始值进行&操作,归并后的结果就是delete按钮是否可用的布尔值。
”合并“(mergyAll)和”归并“(reduce)解决了我们遇到的问题,于是第一种写法就出来了,伪代码如下:
let permissions: Array<any> = []; foreach(checkedItem of checkedItems) { p$: Observable<any> = getDeletePermissionFromAPI(checkedItem.Id); permissions.push(p$); } Observable.from(permissions).mergyAll().reduce((isable: boolean,permission) => { return isable && permission; });
用mergyAll是唯一的解决方案吗,有没有其他操作符也能解决问题呢?其实处理多个流合并的问题,还有zip,concatAll,combineLatest等等操作符可用。我这里并不focus在比较它们的异同点,只是举出还有别的可能性,比如以zip为例,我们可以得到第二种写法:
let permissions: Array<any> = []; foreach(checkedItem of checkedItems) { p$: Observable<any> = getDeletePermissionFromAPI(checkedItem.Id); permissions.push(p$); } Observable.zip(...permissions).reduce((isable: boolean,permission) => { return isable && permission; });
这里补充说明一下,由于zip的参数是若干个流,所以我们这里把流的数组permssions通过spread(...)来打散为元素,这里是一个技巧。
下面我们就来关注一下双层流的问题了,以上我们只是跳入到checkeditems中来想解决方案,现在我们跳到外层来,实际上checkeditems也是一个流,它发射一组checkeditems然后我们去处理来得到一个是否可用的逻辑值。很容易能够想到用map操作符,把checkeditems map为一个boolean,伪代码如下:
this.service.getCheckedItems$().map(checkedItems: any => { //上述delete逻辑,此处省略 ...... })
我们上述的delete的逻辑,我们把checkeditems先变成了流的数组,然后通过一系列的操作符转换为了一个包含最后逻辑值的流。也就是说,外层流checkeditems的每一个值都映射为了一个包裹着逻辑值的新的流,这就是双层流的嵌套问题,disable属性的流每次发射的元素应该只是我们计算出的逻辑值,而不是一个新的流,那么怎么解决这个问题呢,switchMap是解决这种问题的利器。map是将流中的元素转化为另一个元素,switchMap是将一个流转化为另外一个流,由于我们产生了新的流,那么就用switchMap从主流转到从流上,伪代码如下:
this.service.getCheckedItems$().switchMap(checkedItems: any => { //上述delete逻辑,此处省略 ...... })
使用switchMap操作符把主流转化为从流
至此差不多解决了我们的所有问题,至少功能这部分是能够实现了,但是我们的代码还是有很多地方值得优化的:
优化点1: 我们之前新建一个数组并用foreach循环把checkeditems数组变为另外一个包含着权限流的数组,其实就是从一个数组变成另外一个数组。实际上数组里面也有一个map函数,可以把旧数组的每一个元素映射为一个新的元素从而组成一个新数组,正好和我们的场景是吻合的,而且写法更加简洁,避免使用for循环显得代码繁琐。
优化点2: 我们之前是每次都调用API去获取checkeditem的权限,实际上用户可能会频繁进行选择操作,就会多次触发checkitems,而每次都要去API访问。实际上,页面上的macro也就那么几个,我们完全可以一次性的拉去所有macro的权限并存储起来,然后当checkitems触发了后,我们就从本地拿权限就好了,之前的做法有太多不必要的API访问,这是很耗资源和性能的。
对以上两点进行优化以后,我们给出了最终版的伪代码如下:
/** 一次性访问API获取所有macro的权限,并以键值对的方式存储在一个对象中 **/ let allPermissions = {}; foreach(macro in macros) { let p$ = this.service.getPermissionFromAPI(macro.Id); allPermissions[macro.Id] = p$; } this.service.getCheckedItem$.map(checkedItems => { return checkedItems.map((checkedItem) => { return allPermissions[checkedItem.Id] }) }).switchMap(permissions$ => { return Observable.zip(...permissions$).reduce((isable: boolean,permission) => { return isable && permission; },true); })
至此,delete的所有问题都得到了比较完美的解答,其实edit的逻辑也是类似的,而且比delete更简单,可以留给读者自己思考怎么来写。最后我们总结一下,通过这个小案例我们有哪些收获: