高级-无阻塞调用(精)
Opened this issue · 0 comments
无堵塞调用
在之前的章节中,我们看到了take Effect让我们可以在一个集中的地方更好地描述一个非常规的流程。
初次尝试
到目前为止,我们拥有所有需要的Effects用来实现上诉流程。我们可以使用take Effect等待store中指定的action。我们也可以使用call Effect进行同步调用,最后使用put Effect来发起action到store。
例子
import { take, call, put } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
const = token = yield call(Api.authotize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
return token
}catch(error) {
yield put({type: 'LOGIN_ERROR', error})
}
}
function* loginFlow() {
while(true) {
const { user, password } = yield take('LOGIN_REQUEST')
const token = yield call(authorize, user, password)
if(token) {
yield call(Api.storeItem({token}))
yield take('LOGOUT')
yield call(Api.clearItem('token')
}
}
}
首先我们创建了一个独立的Generator authorize,它将执行真实的API调用并在成功后通知Store。
loginFlow 在一个while循环中实现它所有的流程,这样做的意思是: 一旦到达流程最后一步(LOGOUT),通过等待一个新的LOGIN_REQUEST action来启动一个新的迭代。
loginFlow首先等待一个LOGIN_REQUEST ation。然后在action的payload中获取有效凭据(即user和password)并调用一个call到authorize任务。
正如你注意到的,call不仅可以用来调用返回Promise的函数。我们也可以用它调用其他Generator函数。在上面的例子中,loginFlow将等待authorize直到它终止或返回(即执行api调用后,发起action然后返回token至loginFlow)
如果Api调用成功了,authorize将发起一个LOGIN_SUCCESS action然后返回获取到的token。如果调用导致了错误,竟会发起一个LOGIN_ERROR action
如果调用authorize成功,loginFlow将在DOM storage中存储返回的token,并等待LOGOUT action。当用户登出,我们删除存储的token并等待一个新的用户登录。
在authorize失败的情况下,它将返回一个undefined值,这将导致loginFlow跳过当前处理进程并等待一个新的LOGIN_REQUEST action
....
fork
我们需要的是一些非阻塞调用authorize的方法,这样loginFlow就可以继续执行,并且监听并发的活响应未完成之前发出的LOGOUT acton
为了表示无阻塞调用,redux-saga提供可另外一个Effect: fork、当我们fork一个任务,任务会在后台启动,调用者也可以继续它自己的流程,而不是等待被fork的任务结束。
所以为了让loginFlow不错过一个并发的LOGOUT,我们不应该使用call调用authorize任务,而应该使用fork。
import { fork, call, take, put } from 'redux-saga/effects'
function* loginFlow() {
while(true) {
...
const ?? = yield fork(authorize, user, password)
}
}
现在的问题是,自动authorize的action在后台启动之后,我们获取不到token的结果,所以我们需要将token存储操作移到authorize任务内部。
import { fork, call, take, put } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
}catch(error) {
yield put({type: 'LOGIN_ERROR', error})
}
}
function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
yield fork(authorize, user, password)
yield take(['LOGOUT', 'LOGIN_ERROR'])
yield call(Api.clearItem('token'))
}
}
我们使用yield take(['LOGOUT','LOGIN_ERROR'])。意思就是监听2个并发的action
- 如果
authorize
任务在用户登出之前成功了,它将会发起一个LOGIN_SUCCESS
action 然后结束。 然后loginFlow
Saga 只会等待一个未来的LOGOUT
action 被发起(因为LOGIN_ERROR
永远不会发生)。 - 如果
authorize
在用户登出之前失败了,它将会发起一个LOGIN_ERROR
action 然后结束。 那么loginFlow
将在LOGOUT
之前收到LOGIN_ERROR
,然后它会进入另外一个while
迭代并等待下一个LOGIN_REQUEST
action。 - 如果在
authorize
结束之前,用户就登出了,那么loginFlow
将收到一个LOGOUT
action 并且也会等待下一个LOGIN_REQUEST
。
但还是没完,如果我们在API调用期间收到一个LOGOUT action,我们必须取消authorize处理进程,否则将有2个并发的任务,并且authorize任务就会继续运行,并在成功的响应后发起一个LOGIN_SUCCESS action,而将导致状态不一致。
为了取消fork任务,我们可以使用一个指定的Effect cancel
import { take, put, call, fork, cancel } from 'redux-saga/effects'
function* loginFlow() {
while(true) {
const {user,password} = yield take('LOGIN_REQUEST')
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if(action.type === 'LOGOUT')
yield cancel(task)
yield call(Api.clearItem('token'))
}
}
yield fork 的返回结果是一个Task Object,我们将它们返回的对象赋给一个本地常量task。如果我们收到一个LOGOUT action,我们将那个task传入给cancel Effect。如果任务仍在运行,它会被终止。如果任务已经完成,那什么也不会发生,取消操作将会是一个空操作、
cancel Effect不会粗暴地结束我们的authorize任务,相反它会给予一个机会执行清理的逻辑。在finally区块可以处理任何的取消逻辑。由于finally区块执行在任何类型的完成上,如果你想要为了取消做特殊处理,有一个cancelled Effect:
import { take, call, put, cancelled } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
yield call(Api.storeItem, {token})
return token
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
} finally {
if (yield cancelled()) {
}
}
}