redux-observable的实现思路和简单封装

423 阅读2分钟

写了一段时间saga后,实在觉得这不太顺手。比如

  • 一开始就必须写generator函数,来封装异步流程;
  • 使用take/takeEvery等方法来监听action,必须在generator函数中使用;
  • 取消订阅要依靠fork返回的task;
  • 错误处理,除非你加一个中间件全包,否则每当你fork启动了一个新的task,你外层的try/catch则不再生效;而一旦出错没有处理,那么该action对应的saga树将无法响应之后的dispatch事件;
  • 判断任务是否被取消,也得跟在错误处理的finally区块(如果在task层可以通过task.isCancelled() 判断);
  • 如果你想知道dispatch一个saga effect任务什么时候完成,要么传递回调函数,要么内部转发新的action,但最好可能还是需要自己封装:从payload传入一个promise的resolve,通过task.done.then(resolve)来消费。而这都是因为saga还是dva没有管理好dispatch导致原来返回的promise成了摆设。但更加噩梦的时,即使像前面封装了一个新的dispatch来注入promise,但在generator内部如果调用了put转发到其他action,就再一次没辙了,除非继续通过payload维护resolve。

倒不是说saga有什么事情是做不到的,不过看看上面的表现,就是不太能欣赏得来的情况

找到rxjs-observable官网,看完首页的小例子,倍感震撼,说一下其实现的思路(或者说如果我们自己用rxjs如何去设计)注意下面的代码并不官方

  • 首先我们需要遵循redux的设计范式,为了结合redux,至少需要几个条件:1是action监听机制,2是异步流程控制,3收集所有reducer的结果

  • rxjs对于异步天然优秀,只要拿到dispatch就可以异步地提交变更,这个不是问题

  • 那么如何监听action?真的十分的简单:

  • 首先将全局的dispatch事件都统一到一个唯一信号源action$

  • 就比方说,action是一个Subject,那么dispatch方法内部只要调用action是一个Subject,那么dispatch方法内部只要调用`action.next(action)`就完事了

  • 然后在各个业务组件中写reducer时过滤出自定义的type:

  • reducer1 = $action.pipe(filter({type} => type === 'MY_TYPE')).map(() => newState )

  • 这样就完成了action的监听以及返回新state,没有任何新的东西,都是rxjs本身的操作符

  • 最后如何收集各个reducer返回的状态,去更新store?只需要:

  • $action.pipe(merge(reducer1, reducer2, ...)).subscribe(newState => // do update...)

  • 这里的merge,就可以把所有的reducer都融合成一个信号源,因为假定你不会在不同的reducer中写重复的type,那么这个最终merge出来的信号,在每次的dispatch事件中,都只会得到一个唯一的newState,这样就完成了收集,这其实就是combineReducer的表现

  • 寥寥几个操作符就完成了工作,subject,pipe,filter,map,merge这些都是rxjs中十分常见的方法,不需要任何新的概念

而关于Rxjs的异步操作符,错误处理,取消订阅这些本身的功能,就不过多赘述了,无缝接入使用即可,而这些都是前端应用中非常重要的

事实上,上面只是我的想法,而rxjs-observable并不是这样用的(但最重要的原理差不多)

rxjs-observable中$action的返回的依然是action对象,这个过程被定义为epic,也就是说,epic最终还是转发到原始的reducer函数去计算state

我只是觉得,既然我们用了$action,那直接在这个过程中返回最新state就好了,不应该再写原始reducer

但是saga也是这样的,effect最后也要转发给reducer

为什么是这样的?注意到redux-observable这个名字,首先这就是为redux服务的。redux的文档中写了:Dispatches an action. It is the only way to trigger a state change. 所以别无他法。其实和saga一样,这些第三方工具,只不过是redux生态的衍生品,他们只需要遵循action -> action的方式,就完成他们的责任了,他们当然不需要去操redux的心。

但无论如何,有两个概念,并不是一个优雅的情况,本不需要这么多的吧。我还是觉得有了epic,就不再需要形式上的reducer多好。

Mixin Promise

再回到前面另一个问题,dispatch后如何知道任务完成,这个实在是很棘手,dispatch不返回promise这能用吗?dva做了封装吧,但是有问题。而rxjs-observable也没封装,是怕封装了适应场景就减少了吗?如果要在各个异步action之间维护一个promise,唯一的方式就是通过payload,那么如果要避免resolve被提前消费,还可能还需要加入新的代码去做标记阻止。可恶啊,这是redux的问题。但再想想,如果某个action内,又dispatch了多个异步action呢?这次是要等所有的子action都完成吗?问题开始变大了。再仔细想想,这其实是一个意义不明的问题,并不是框架的问题,因为用户意图无法被确定。所以要封装的话,那就只封装一层action吧,再多就无需理会了,如果这能照顾到绝大多数的情况,那也足够了。

好吧,妥协了,只封装一层action也是可以接受的。那么如何mixin一个promise?可以写一个新的dispatch,改造payload参数。下面是简单代码实现。但真实情况下还要考虑payload可能不为对象或不传的情况

export const dispatchPromise = action => {  
    const ret = new Promise(res => action.payload._res = res);  
    store.dispatch(action);  
    return ret;
}

如果喜欢rxjs风格,还能最后使用fromPromise(ret)来返回observable,这样风格就更加统一了

最后只需要在action$最后拼接tap去消费resolve即可

const customize = (epic, type) =>  action$ => {    
    let resolve = null;    
    return epic(action$.pipe(      
        ofType(type),      
        tap(action => resolve = action.payload._res),    
    )).pipe(      
        tap(() => resolve())    
    )  
}

取消订阅

官网是使用takeUtil(action$.pipe(ofType('mySignal'))),这个好巧妙,但我还是不喜欢这样,原因是假如我有两个组件同时发出了dispatch,但想要取消其中一个怎么办?办不了吧,因为你使用的是一个固定的type作为信号。所以应该像前面封装了一个新的dispatch方法一样,再写一个dispatchCancellable,当然这些都可以统一封装到一个更加公共的方法里。

总之思路就是在dispatch的时候传入一个信号源,比方:

const [destory$] = useState(new Subject())
useEffect(() => () => destory$.next()) // 组件卸载时发出信号
const func = () => dispatchCancellable(action, destory$) 
// destory$和前面一样从payload传入,customize中在ofType之后通过takeUtil消费即可