redux-saga
redux-saga解决的问题就是:避免redux-thunk,redux-promise给action的带来的不纯粹性,dispatch调用的时候的不一致性,减少写测试代码时候的复杂性。
redux-saga主要使用的技术就是ES6出的Generator,用yield来控制代码的执行。
基本使用
和普通的中间件的使用方式一样 。只不过不能直接使用,需要从redux-saga中导出一个创建中间件的方法。
import createSagaMiddleware from 'redux-saga'import mySaga from './sagas/mySaga'const sagaMiddleware = createSagaMiddleware()const store = createStore(rootReducer, applyMiddleware(sagaMiddleware, logger))// 与普通的中间件不同的地方就在于这里,需要执行run方法。将Middleware连接StoresagaMiddleware.run(mySaga)
实现一个saga函数,saga函数其实就是一个生成器生成函数。
function* fetchUser(action: AnyAction) {console.log(action, '我是传递的action')try {const user: Record<string, any> = yield delay(1000)put(createUpdateUserAction(nanoid(), user))} catch (error) {console.log('错误', error)}}export default function* () {yield takeEvery(FETCH_USER, fetchUser)}
其中FETCH_USER是action的type,fetchUser是一个新的生成器函数,里面进行异步数据的获取。
其中几个方法的说明:
takeEvery:redux-saga提供的一个指令。能够监听store.dispatch触发这个指令监听的action的type。然后就会执行第二个参数的生成器函数。put:用于执行与dispatch相同的功能。delay:redux-saga提供的延迟方法。
而外界调用还是跟原来一样:
store.dispatch(createFetchUserAction({id: nanoid(),name: 'test'}))
当然,我们也可以一次性监听很多的Action的type。不过一般来说,我们也只会监听异步的Action的type。
同步的话,没有这么麻烦,直接调用改变相应的state就可以了。
take和takeEvery
take是比较低层级的Api,它不会帮助我们做些什么事情,只会监听到某个Action的type,然后我们拿到相应的Action,要做什么我们自己决定。同时它是阻塞性的。且是一次性的,只会监听一次。
function* mySaga() {while (true) {// 调用dispatch后就卡在这里,不会进行接下来的工作const action: unknown = yield take(FETCH_USER)console.log(action as ReturnType<typeof createFetchUserAction>, 'action')}}
这样的话有个好处就是:
take后面的代码阻塞后不会执行。我们有一段流程是比较固定的话,使用这个的话能很快的知道大致流程是怎么样的。例如:
经过上面这样写的话,流程就很清楚了。一下子就知道当前是什么状态。function* loginFlow() {while (true) {yield take('LOGIN')// ... perform the login logic// 在这里实现或者调用登录的逻辑,然后会卡在下面的take中yield take('LOGOUT')// ... perform the logout logic// 登出之后,在这里写相应的逻辑。然后执行完成// 等待登录的action}}
这其实是监听未来的action,因为take只监听了action,但是没有对应的处理函数去处理。
而不像takeEvery一开始就必须针对某一个特定的action做出相应的处理。
一个好的 **UI** 程序应该始终强制执行顺序一致的 **actions**,通过隐藏或禁用意料之外的 **action**
all指令
all指令也是阻塞的,只有等到里面的所有的任务(生成器都执行完成后)都执行完成后,才会执行完成。
export default function* () {yield all([counterTask(), studentTask()])console.log("saga 完成")}
fork指令
fork指令最大的作用就是新开一个任务,不阻塞当前的后面代码执行。但是当前的上下文的生成器函数,必须要等到fork执行完成后才能整体的完成。有点像新开了一个线程。
假如有如下的任务需要执行:
function* fetchAll() {const task1 = yield fork(handleDelay)const task2 = yield fork(handleDelay)const task3 = yield delay(500)}
task1和task2的执行不影响task3的执行,不会进行阻塞。
但是fetchAll任务的完成情况会受到三个任务都完成,这个才会完成。所以,fetchAll这个任务,与下面的写法是一致的。
yield all([call(handleDelay), call(handleDelay), delay(500)])
如果用了fork的话,fetchAll进行错误捕获,而是要在使用fetchAll的地方进行错误捕获。因为当handleDelay报错的时候,handleDelay就会被取消,错误将由调用fetchAll的地方进行捕获。
cancel
用于取消一个或多个任务,实际上,取消的实现原理,是利用generator.return。
一旦任务被 fork,可以使用 yield cancel(task) 来中止任务执行。取消正在运行的任务。
如果cancel没有参数,则取消当前任务线(包括他的parent的任务,所有的都会被取消)。一般不会使用,除非是走到某个判断的时候,整个任务都不执行了。
- 正常的取消
fork任务 ```typescript function* handleDelay() { console.log(‘我取消完了’) // 1 yield delay(2000) yield call(getAllStudents) console.log(‘我取消了’) // 不会打印 }
function* fetchAll() { const task1: Task = yield fork(handleDelay) yield cancel(task1) yield delay(1000) console.log(‘我是取消’) // 2 }
export default function* () { try { yield call(fetchAll) } catch (error) { console.warn(error) } console.log(‘我不阻塞’) // 3 }
- 取消了这个任务,并不是表示这个任务里面的代码不会执行了。而是表示这个任务取消之后的代码不会执行了。- 如果想要在被取消的任务里面执行某一些代码。我们就进行`try{}finnally{}`,可以在`finally`里面进行相应的取消。但是`finally`里面的代码,无论是否取消都会执行,所以,一般我们会配置`cancelled`这个`api`进行判断,如果取消了,那么就执行某些代码,如果没有取消就执行什么代码:具体代码如下:```typescriptfunction* handleDelay() {try {console.log('我马上取消')yield delay(1000)yield call(getAllStudents)console.log('我取消完了')} finally {const hasCancle: boolean = yield cancelled()console.log('我被取消了,完毕了', hasCancle)}}
