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连接Store
sagaMiddleware.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`进行判断,如果取消了,那么就执行某些代码,如果没有取消就执行什么代码:具体代码如下:
```typescript
function* handleDelay() {
try {
console.log('我马上取消')
yield delay(1000)
yield call(getAllStudents)
console.log('我取消完了')
} finally {
const hasCancle: boolean = yield cancelled()
console.log('我被取消了,完毕了', hasCancle)
}
}