当前位置: 首页 > 科技观察

深入理解Redux数据流与异步进程管理

时间:2023-03-22 11:07:54 科技观察

本文转载自微信公众号《神光的编程秘籍》,作者神说,必有光zxg。转载本文请联系神光编程秘籍公众号。前端框架的数据流前端框架实现了数据驱动视图变化的功能。我们使用模板或者jsx来描述数据和视图的绑定关系,那么我们只需要关心数据管理即可。数据在组件之间、组件与全局存储之间传递,称为前端框架的数据流。一般来说,除了只有某个组件关心的某一部分状态数据,我们会把状态数据放在组件里,多个组件关心的业务数据和状态数据都会放在store里。组件从store中获取数据,并在交互时通知store更改相应的数据。这个store不一定是redux、mobox等第三方库。其实react内置的context也可以作为store使用。但是,上下文作为商店存在问题。任何一个组件都可以从context中取出数据进行修改,所以排查问题非常困难,因为不知道是哪个组件改变了数据,也就是数据流向不清晰。也正是因为这个原因,我们几乎看不到context作为store的使用,基本上都是搭配redux。那么redux为什么好呢?第一个原因是数据流向清晰,变更数据有统一的入口。在组件中,派发一个action来触发store的修改,修改的逻辑在reducer中。该组件然后监视商店的数据更改并从中检索最新数据。这样,数据流向是单向的,清晰的,易于管理。这也是为什么我们在公司想要的任何权限都必须经过审批流程,而不是直接找人的原因。集中管理流程相对清晰,可追溯。异步过程的管理很多时候改变store数据是一个异步过程,比如等待网络请求返回数据,定时改变数据,等待某个事件改变数据等,这些异步过程的代码在哪里放置?成分?放在一个组件中是可以的,但是如何跨组件复用这个异步过程呢?如何控制多个异步进程之间的串行和并行?所以当异步进程比较多,并且异步进程之间不是相互独立的,存在串行、并行,甚至更复杂的关系时,直接将异步逻辑放在组件中是不行的。如果你不把它放在组件中,你应该把它放在哪里?能否利用redux提供的中间件机制来放这些异步进程呢?Redux中间件先来看看什么是redux中间件:redux的流程很简单,就是dispatch一个action去store,reducer去处理action。那么如果你想在到达商店之前做更多的处理呢?在哪里添加?转型派遣!中间件的原理就是一层一层的打包dispatch。下面是applyMiddleware的源码。可以看到applyMiddleware对store.dispatch层层包裹,修改dispatch后最终返回store。functionapplyMiddleware(middlewares){letdispatch=store.dispatchmiddlewares.forEach(middleware=>dispatch=middleware(store)(dispatch))return{...store,dispatch}}所以中间件返回的函数就是处理action的dispatch:functionmiddlewareXxx(store){returnfunction(next){returnfunction(action){//xx};};};}中间件会把dispatch包裹起来,dispatch就是把action传递给store,中间件自然可以获取操作,获取商店,然后是打包的调度。比如redux-thunk中间件的实现:functioncreateThunkMiddleware(extraArgument){return({dispatch,getState})=>next=>action=>{if(typeofaction==='function'){returnaction(dispatch,getState,extraArgument);}returnext(action);};}constthunk=createThunkMiddleware();它判断如果action是一个函数,就执行这个函数,传入store.dispath和store.getState,否则传给内部dispatch。通过redux-thunk中间件,我们可以把异步过程以函数的形式放在dispatch参数中:constlogin=(userName)=>(dispatch)=>{dispatch({type:'loginStart'})request.post('/api/login',{data:userName},()=>{dispatch({type:'loginSuccess',payload:userName})})}store.dispatch(login('guang'))但这解决了是不是很难在组件中复用异步进程,多个异步进程之间很难做并行和串行控制?不是的,这个逻辑还是写在了组件里面,只是移到了dispatch中,并且也没有提供管理多个异步进程的机制。要解决这个问题,需要使用redux-saga或者redux-observable中间件。redux-sagaredux-saga并没有改变action,它会透明地将action传递给store,只是增加了一个额外的异步过程。redux-saga中间件是这样启用的:},applyMiddleware(sagaMiddleware))sagaMiddleware.run(rootSaga)需要调用run运行saga的watchersaga:watchersaga监听一些动作,然后调用workersaga处理:import{all,takeLatest}from'redux-saga/effects'function*rootSaga(){yieldall([takeLatest('login',login),takeLatest('logout',logout)])}exportdefaultrootSagaredux-saga会先将action透传给store,然后判断是否是action是taker监听的:functionsagaMiddleware({getState,dispatch}){returnfunction(next){returnfunction(action){constresult=next(action);//将action传递给storechannel.put(action);//triggersaga动作监听进程returnresult;}}}当发现action被监听,然后执行对应的taker调用workersaga处理:o})}catch(error){yieldput({type:'loginError',error})}}function*logout(){yieldput({type:'logoutSuccess'})}比如login和logout会有不同的workersagalogin会请求登录界面,然后触发loginSuccess或loginError动作。注销将触发注销成功操作。reduxsaga的异步流程管理是这样的:首先将action透传给store,然后判断action是否被taker监听,如果是,则调用相应的workersaga进行处理。reduxsaga除了redux的action流程外,还增加了一个监听action的异步流程。其实整个过程还是比较容易理解的。理解成本较高的方法是生成器:比如下面的代码:同样的东西,相当于takeEvery:function*xxxSaga(){yieldtakeEvery('xxx_action');//...}但是因为有while(true),所以很多同学不明白,这个没死它在循环吗?没有。生成器执行完后,返回一个迭代器,另一个程序需要调用next方法继续执行。所以如何执行,是否继续执行,都是由另外一个程序来控制的。在redux-saga中,控制workersaga执行的程序称为task。workersaga只是通过call、fork和put(这些命令称为效果)等命令告诉任务要做什么。然后任务会调用不同的实现函数来执行workersaga。为什么要这样设计?直接执行它是不够的。为什么要拆分成workersaga和task?理解成本会不会很高?确实,将其设计成生成器的形式会增加理解成本,但换来的是Testability。因为各种sideeffect,比如网络请求,dispatchactiontostore等等,都变成了call和put这样的effect,执行由task部分控制。然后怎么执行可以随意切换,这样测试的时候只需要模拟传入相应的数据,就可以测试workersaga了。reduxsaga作为生成器的设计是学习成本和可测试性之间的权衡。还记得redux-thunk有什么问题吗?无法处理多个异步进程之间的并行和串行复杂关系。redux-saga是怎么解决的呢?redux-saga提供了all、race、takeEvery、takeLatest等effect来指定多个异步进程之间的关系:比如takeEvery会对多个action中的每一个做相同的处理,而takeLatest会去处理多个action中的最后一个,race只会返回最快的异步过程的结果,依此类推。这些控制多个异步进程之间关系的效果,正是redux-thunk所不具备的,也是管理复杂异步进程必不可少的部分。所以redux-saga可以管理复杂的异步流程,并且具有很好的可测试性。其实最著名的异步进程管理就是rxjs,而redux-observable是基于rxjs实现的,也是一个复杂的异步进程管理方案。redux-observableredux-observable和redux-saga非常相似,比如启用插件的部分:constepicMiddleware=createEpicMiddleware();conststore=createStore(rootReducer,applyMiddleware(epicMiddleware));epicMiddleware.run(rootEpic);和reduxsaga的启动过程是一样的,只是不叫saga而是叫epic。但是对于异步过程的处理,reduxsaga自己提供了一些效果,redux-observable使用了rxjs操作符:import{ajax}from'rxjs/ajax';constfetchUserEpic=(action$,state$)=>action$.pipe(ofType('FETCH_USER'),mergeMap(({payload})=>ajax.getJSON(`/api/users/${payload}`).pipe(map(response=>({type:'FETCH_USER_FULFILLED',payload:response}))));使用ofType指定监听的action,处理后将action返回store。相对于redux-saga,redux-observable支持的异步流程更丰富,直接连接了生态operator是开放的,而redux-saga只提供了几个内置的effect进行处理,所以在做特别复杂的异步流程处理的时候,redux-observable可以更明显的利用rxjsoperators的优势。但是redux-saga的优势在于基于generator的可测试性,在大多数场景下,redux-sa提供的异步进程的处理能力ga就够了,所以相对来说,redux-saga用的更多。总结前端框架实现了数据到视图的绑定,我们只需要关心数据流向即可。相比context混乱的数据流,redux的view->action->store->view这种单向的数据流更加清晰易管理。前端代码中有很多异步过程。这些异步过程可能具有串行、并行甚至更复杂的关系。在组件中管理它们并不容易。它们可以放在redux中间件中。redux中间件就是dispatch的层层封装。比如redux-thunk判断下一个action是函数就执行,否则继续派发。redux-thunk没有提供管理多个异步进程的机制,必须使用redux-saga或者redux-observable来管理复杂的异步进程。redux-saga将action透传给store,监听action执行相应的异步流程。异步过程的描述使用生成器的形式,具有可测试性的优点。比如通过take、takeEvery、takeLatest监听动作,然后执行workersaga。Workersaga可以使用put、call、fork等effect来描述不同的sideeffects,由task执行。redux-observable也监听action来执行相应的异步流程,不过它是一个基于rxjs的operator。与saga相比,异步进程的管理功能更加强大。无论是redux-saga通过generator组织异步进程,通过内置effects处理多个异步进程之间的关系,还是redux-observable通过rxjs算子组织异步进程与多个异步进程之间的关系。它们都解决了复杂的异步流程处理问题,可以根据场景的复杂程度灵活选择。