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的对象,便于后续的遍历和执行,在此不具体分析。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。