bai3/note

高级-无阻塞调用(精)

Opened this issue · 0 comments

bai3 commented

无堵塞调用

在之前的章节中,我们看到了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()) {
    }
  }
}