redux-saga 初识和使用
redux-saga是一个管理Redux应用异步操作的中间件,功能类似redux-thunk+async/await,它通过创建Sagas将所有的异步操作逻辑存放在一个地方进行集中处理。
redux-saga的effects
redux-saga中的Effects是一个纯文本JavaScript对象,包含一些将被sagamiddleware执行的指令。这些指令所执行的操作包括如下三种:
- 发起一个异步调用(如发一起一个Ajax请求)
- 发起其他的action从而更新Store
- 调用其他的Sagas
Effects中包含的指令有很多,具体可以异步API参考进行查阅
redux-saga的特点
方便测试,例如:
assert.deepEqual(iterator.next().value,call(Api.fetch,'/products'))
- action可以保持其纯净性,异步操作集中在saga中进行处理
- watch/worker(监听->执行)的工作形式
- 被实现为generator
- 对含有复杂异步逻辑的应用场景支持良好
- 更细粒度地实现异步逻辑,从而使流程更加清晰明了,遇到bug易于追踪和解决。
- 以同步的方式书写异步逻辑,更符合人的思维逻辑
- 从redux-thunk到redux-saga
假如现在有一个场景:用户在登录的时候需要验证用户的username和password是否符合要求。
使用redux-thunk实现
获取用户数据的逻辑(user.js):
//user.js importrequestfrom'axios'; //defineconstants //defineinitialstate //exportdefaultreducer exportconstloadUserData=(uid)=>async(dispatch)=>{ try{ dispatch({type:USERDATA_REQUEST}); let{data}=awaitrequest.get(`/users/${uid}`); dispatch({type:USERDATA_SUCCESS,data}); }catch(error){ dispatch({type:USERDATA_ERROR,error}); } }
验证登录的逻辑(login.js):
importrequestfrom'axios'; import{loadUserData}from'./user'; exportconstlogin=(user,pass)=>async(dispatch)=>{ try{ dispatch({type:LOGIN_REQUEST}); let{data}=awaitrequest.post('/login',{user,pass}); awaitdispatch(loadUserData(data.uid)); dispatch({type:LOGIN_SUCCESS,data}); }catch(error){ dispatch({type:LOGIN_ERROR,error}); } }
redux-saga
异步逻辑可以全部写进saga.js中:
exportfunction*loginSaga(){ while(true){ const{user,pass}=yieldtake(LOGIN_REQUEST)//等待Store上指定的actionLOGIN_REQUEST try{ let{data}=yieldcall(loginRequest,{user,pass});//阻塞,请求后台数据 yieldfork(loadUserData,data.uid);//非阻塞执行loadUserData yieldput({type:LOGIN_SUCCESS,data});//发起一个action,类似于dispatch }catch(error){ yieldput({type:LOGIN_ERROR,error}); } } } exportfunction*loadUserData(uid){ try{ yieldput({type:USERDATA_REQUEST}); let{data}=yieldcall(userRequest,`/users/${uid}`); yieldput({type:USERDATA_SUCCESS,data}); }catch(error){ yieldput({type:USERDATA_ERROR,error}); } }
难点解读
对于redux-saga,还是有很多比较难以理解和晦涩的地方,下面笔者针对自己觉得比较容易混淆的概念进行整理:
take的使用
take和takeEvery都是监听某个action,但是两者的作用却不一致,takeEvery是每次action触发的时候都响应,而take则是执行流执行到take语句时才响应。takeEvery只是监听action,并执行相对应的处理函数,对何时执行action以及如何响应action并没有多大的控制权,被调用的任务无法控制何时被调用,并且它们也无法控制何时停止监听,它只能在每次action被匹配时一遍又一遍地被调用。但是take可以在generator函数中决定何时响应一个action以及响应后的后续操作。
例如在监听所有类型的action触发时进行logger操作,使用takeEvery实现如下:
import{takeEvery}from'redux-saga' function*watchAndLog(getState){ yield*takeEvery('*',function*logger(action){ //dosomeloggeroperation//在回调函数体内 }) }
使用take实现如下:
import{take}from'redux-saga/effects' function*watchAndLog(getState){ while(true){ constaction=yieldtake('*') //dosomeloggeroperation//与take并行 }) }
其中while(true)的意思是一旦到达流程最后一步(logger),通过等待一个新的任意的action来启动一个新的迭代(logger流程)。
阻塞和非阻塞
call操作是用来发起异步操作的,对于generator来说,call是阻塞的操作,它在Generator调用结束之前不能执行或处理任何其他事情。,但是fork却是非阻塞操作,当fork调动任务时,该任务会在后台执行,此时的执行流可以继续往后面执行而不用等待结果返回。
例如如下的登录场景:
function*loginFlow(){ while(true){ const{user,password}=yieldtake('LOGIN_REQUEST') consttoken=yieldcall(authorize,user,password) if(token){ yieldcall(Api.storeItem({token})) yieldtake('LOGOUT') yieldcall(Api.clearItem('token')) } } }
若在call在去请求authorize时,结果未返回,但是此时用户又触发了LOGOUT的action,此时的LOGOUT将会被忽略而不被处理,因为loginFlow在authorize中被堵塞了,没有执行到take('LOGOUT')那里
同时执行多个任务
如若遇到某个场景需要同一时间执行多个任务,比如请求users数据和products数据,应该使用如下的方式:
import{call}from'redux-saga/effects' //同步执行 const[users,products]=yield[ call(fetch,'/users'), call(fetch,'/products') ] //而不是 //顺序执行 constusers=yieldcall(fetch,'/users'), products=yieldcall(fetch,'/products')
当yield后面是一个数组时,那么数组里面的操作将按照Promise.all的执行规则来执行,genertor会阻塞知道所有的effects被执行完成
源码解读
在每一个使用redux-saga的项目中,主文件中都会有如下一段将sagas中间件加入到Store的逻辑:
constsagaMiddleware=createSagaMiddleware({sagaMonitor}) conststore=createStore( reducer, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga)
其中createSagaMiddleware是redux-saga核心源码文件src/middleware.js中导出的方法:
exportdefaultfunctionsagaMiddlewareFactory({context={},...options}={}){ ... functionsagaMiddleware({getState,dispatch}){ constchannel=stdChannel() channel.put=(options.emitter||identity)(channel.put) sagaMiddleware.run=runSaga.bind(null,{ context, channel, dispatch, getState, sagaMonitor, logger, onError, effectMiddlewares, }) returnnext=>action=>{ if(sagaMonitor&&sagaMonitor.actionDispatched){ sagaMonitor.actionDispatched(action) } constresult=next(action)//hitreducers channel.put(action) returnresult } } ... }
这段逻辑主要是执行了sagaMiddleware(),该函数里面将runSaga赋值给sagaMiddleware.run并执行,最后返回middleware。接着看runSaga()的逻辑:
exportfunctionrunSaga(options,saga,...args){ ... consttask=proc( iterator, channel, wrapSagaDispatch(dispatch), getState, context, {sagaMonitor,logger,onError,middleware}, effectId, saga.name, ) if(sagaMonitor){ sagaMonitor.effectResolved(effectId,task) } returntask }
这个函数里定义了返回了一个task对象,该task是由proc产生的,移步proc.js:
exportdefaultfunctionproc( iterator, stdChannel, dispatch=noop, getState=noop, parentContext={}, options={}, parentEffectId=0, name='anonymous', cont, ){ ... consttask=newTask(parentEffectId,name,iterator,cont) constmainTask={name,cancel:cancelMain,isRunning:true} consttaskQueue=forkQueue(name,mainTask,end) ... next() returntask functionnext(arg,isErr){ ... if(!result.done){ digestEffect(result.value,parentEffectId,'',next) } ... } }
其中digestEffect就执行了effectTriggerd()和runEffect(),也就是执行effect,其中runEffect()中定义了不同effect执行相对应的函数,每一个effect函数都在proc.js实现了。
除了一些核心方法之外,redux-saga还提供了一系列的helper文件,这些文件的作用是返回一个类iterator的对象,便于后续的遍历和执行,在此不具体分析。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。