Redux Saga辅助函数

项目代码中,大家人云亦云的使用takeEvery辅助函数,实际上大错特错,应该按需使用。Saga的官方文档确实写的很差,于是翻源码,做测试,对此理解的透彻了些,这里Mark一番。

辅助函数

takeEvery

官方介绍如下

1
2
3
4
5
6
7
8
9
10
* `takeEvery` allows concurrent actions to be handled. In the example above,
* when a `USER_REQUESTED` action is dispatched, a new `fetchUser` task is
* started even if a previous `fetchUser` is still pending (for example, the
* user clicks on a `Load User` button 2 consecutive times at a rapid rate, the
* 2nd click will dispatch a `USER_REQUESTED` action while the `fetchUser` fired
* on the first one hasn't yet terminated)
*
* `takeEvery` doesn't handle out of order responses from tasks. There is no
* guarantee that the tasks will terminate in the same order they were started.
* To handle out of order responses, you may consider `takeLatest` below.

注意

  1. takeEvery不可以保证effects结束顺序与发起顺序相同。
  2. 每发起一个action,即会执行一个effects

takeLeading

1
2
3
4
5
6
* Spawns a `saga` on each action dispatched to the Store that matches
* `pattern`. After spawning a task once, it blocks until spawned saga completes
* and then starts to listen for a `pattern` again.
*
* In short, `takeLeading` is listening for the actions when it doesn't run a
* saga.

注意

  1. takeLeading监听的action,只有不阻塞时,才会执行。换句话说,只有当前action没有在执行的saga,发起才work。

takeLatest

1
2
3
4
5
6
7
8
9
* Spawns a `saga` on each action dispatched to the Store that matches
* `pattern`. And automatically cancels any previous `saga` task started
* previously if it's still running.
*
* Each time an action is dispatched to the store. And if this action matches
* `pattern`, `takeLatest` starts a new `saga` task in the background. If a
* `saga` task was started previously (on the last action dispatched before the
* actual action), and if this task is still running, the task will be
* cancelled.

注意

  1. takeLatest监听的action,如果action被派发,此时有在执行的effects,则会被取消,进而执行新的action。

上例子

1
2
3
4
5
6
7
8
9
function* fetchUserEffects(action) {
console.log(`这是第${action.payload.count}次`);
const userInfo = (yield call(getUserInfo)).data;
yield put(setUserInfoAsync(userInfo));
yield delay(1000);

const userHistory = (yield call(getUserHistory)).data;
yield put(setUserHistory(userHistory));
}

注意到count作为计数器,每点击按钮一次加一,第一次为1。

takeEvery

yield takeEvery('USER_FETCH', fetchUserEffects);,连续发起4次USER_FETCH

注意,action发起了4次,所以可以说明没发起一次action,对应坚挺的saga就会执行一次。

takeLeading

当我们设定为takeLeading,连续发起4次USER_FETCH,注意到只有第一次的被执行。

takeLatest

当我们设定为takeLatest,连续发起4次USER_FETCH,注意到其实saga执行还是4次,但是只有最后一次完整执行

WHY ?

正如上面官网介绍,假如之前的saga还在执行,则会取消,执行最新的。而我们新的点击时,老的saga还停留到delay阶段,所以会立即取消,因此才会出现上述4次打印和userInfo,但只有最后一次的userHistory。

关于call api

假如第一次effects执行到一句call api,正在请求中,状态,那么当发起新的USER_FETCH,那么这个API请求会取消掉吗?不会!取消的意思是老的generator函数不会继续next,但当前进行的动作还是会执行完。

使用建议

  • 假如saga是检索,考虑到用户可能更新了检索条件,这时使用takeLatest,毕竟之前在执行检索已经无意义。
  • 假如saga执行的是计数器之类的,那么每次都会影响记录的数据,这时使用takeEvery
  • 假如saga执行的操作,输入和输出都一定是一样的,那么应该使用takeLeading,毕竟再次重复执行,毫无意义并且增加开销。

一句话,还是要根据实际情况使用。

参考文档