redux-saga管理异步

redux-saga管理异步

Tags
异步编程
协程
Redux
React.js
SagaDuck
状态管理
CreatedTime
Aug 22, 2022 05:31 AM
Slug
2020-12-26-redux-saga
UpdatedTime
Last updated August 22, 2022

redux-saga 是做什么?

redux-saga 基于 yield 语法,能够创建+管理更加复杂的异步操作。
比如有时候点击前端按钮,发起异步请求。为了防止频繁点击请求,需要进行节流防抖。除了可以在发起请求的时候,进行节流防抖。还可以把节流防抖的时机提前,在状态更新的时候节流防抖
怎么理解呢?就是一定时间内的点击,只考虑最近一次的点击。最近这次点击才会触发回调函数,发起异步,更新状态。
redux-saga 就提供了很多这样的操作符来控制更复杂的异步流程。

redux-saga 的简单使用

先看代码,假设要更新用户状态:
sagas.js文件,定义 saga:
/* * @Author: dongyuanxin * @Date: 2020-12-25 18:51:11 * @Github: https://github.com/dongyuanxin/blog * @Blog: https://0x98k.com/ * @Description: 定义saga */ // effects中,有处理异步副作用的操作符 const { put, takeLatest, delay, call } = require("redux-saga/effects"); /** * 异步请求函数:这里简单模拟拿到用户数据 */ function* fetchUser(userId) { yield delay(100); // 模拟异步 return { sex: 1, userId, }; } /** * saga函数:负责用户异步数据的状态维护 */ function* watchUserFetchRequested() { console.log("watch action.type: USER_FETCH_REQUESTED"); yield takeLatest("USER_FETCH_REQUESTED", function* (action) { console.log("invoke action.type: USER_FETCH_REQUESTED"); try { // 通过call调用异步函数,第一个参数是函数,之后都是函数参数 const userInfo = yield call(fetchUser, action.payload); yield put({ type: "USER_FETCH_SUCCEEDED", payload: userInfo }); } catch (error) { yield put({ type: "USER_FETCH_FAILED", payload: error.message }); } }); } module.exports = { watchUserFetchRequested };
index.js文件,使用 saga:
/* * @Author: dongyuanxin * @Date: 2020-12-25 18:42:57 * @Github: https://github.com/dongyuanxin/blog * @Blog: https://0x98k.com/ * @Description: redux-saga学习 */ const { watchUserFetchRequested } = require("./sagas"); const { createStore, applyMiddleware } = require("redux"); // step1: 创建一个saga的中间件 const createSagaMiddleware = require("redux-saga").default; const sagaMiddleware = createSagaMiddleware(); // 这只是一个普普通通的更新状态的 reducer function startUserReq(state = {}, action) { switch (action.type) { // USER_FETCH_REQUESTED 是一个不改动 state 的 action.type // 它主要是用来触发 saga 函数中的事件监听 case "USER_FETCH_REQUESTED": console.log("reducer USER_FETCH_REQUESTED"); return state; case "USER_FETCH_SUCCEEDED": console.log("reducer USER_FETCH_SUCCEEDED"); return action.payload; case "USER_FETCH_FAILED": console.log("reducer USER_FETCH_FAILED"); return action.payload; default: return state; } } // step2: 使用saga中间件 const store = createStore(startUserReq, applyMiddleware(sagaMiddleware)); // step3: 调用run(),使用封装的saga的函数(watchUserFetchRequested) // 在watchUserFetchRequested中,做了什么呢? // 1、通过事件监听(effects/takeLatest函数),监听action.type(USER_FETCH_REQUESTED),然后发起请求 // 2、发起请求后,成功和创建则更新状态。在saga中不通过dispatch发起更新,通过effects/put发起更新 sagaMiddleware.run(watchUserFetchRequested); // step4: 发起获取用户的异步请求 // 1、会先挨个触发reducer,和redux原生一样 // 2、触发完redcuer之后,由于step3.1中,saga中监听了USER_FETCH_REQUESTED,所以会执行回调函数 // 3、剩下的步骤和step3.2描述的一样 store.dispatch({ type: "USER_FETCH_REQUESTED" });
在注释中,展示了整体调用流程。除此之外,还有几点要注意:
  • saga 是基于事件的(例如 take、takeLatest 等等)
  • store.dispatch 还是会先触发 reducer,reducer 执行之后,才会触发 saga 的事件监听回调
  • saga 中,通过 put 而不是 dispatch 来更新触发 reducer,更新状态
  • sage 中,如果事件监听回调中,put 触发 reducer 传入的 action.type 和事件监听的 action.type 一样,就可能会陷入死循环
对于第 2 点的顺序,上述代码的输出是:
watch action.type: USER_FETCH_REQUESTED reducer USER_FETCH_REQUESTED invoke action.type: USER_FETCH_REQUESTED reducer USER_FETCH_SUCCEEDED
对于第 4 点,代码换成以下的样子,就会死循环:
function* watchUserFetchRequested() { console.log("watch action.type: USER_FETCH_REQUESTED"); yield takeLatest("USER_FETCH_REQUESTED", function* (action) { console.log("invoke action.type: USER_FETCH_REQUESTED"); try { const userInfo = yield call(fetchUser, action.payload); // 这里会触发reducer,action.type为USER_FETCH_REQUESTED // 然后外层事件监听又会监听到,死循环 yield put({ type: "USER_FETCH_REQUESTED", payload: userInfo }); } catch (error) { yield put({ type: "USER_FETCH_FAILED", payload: error.message }); } }); }

effects 深入学习

并发任务:all、race

前面多个yield call(...)是串行的,如果想并行怎么写呢?使用all操作符。
const [users, repos] = yield all([ call(fetchUser, { role: 'user' }), call(fetchStudent, { role: 'student' }) ])
effects/allPromise.all的行为类似,effects/racePromise.race的行为类似。

异步任务:fork、spawan

前面yield call(...)是阻塞的,等待 call 中的异步任务完成后,才会向下执行。
如果想异步执行,那么需要使用fork(...),返回异步标识,然后通过effects/cancel来取消。
上面的sagas.js改造下:
function* fetchUser(userId) { yield delay(100); console.log(">>> 触发fetchUser"); if (yield cancelled()) { // 如果fetchUser是异步任务,并且被取消了,这里可以捕获到 } return { sex: 1, userId, }; } function* watchUserFetchRequested() { yield takeLatest("USER_FETCH_REQUESTED", function* (action) { try { const task = yield fork(fetchUser, action.payload); console.log(">>> fork完成"); // 调用 cancel 取消任务 yield cancel(task); yield put({ type: "USER_FETCH_SUCCEEDED", payload: userInfo }); } catch (error) { yield put({ type: "USER_FETCH_FAILED", payload: error.message }); } }); }
上面代码输出是:
>>> fork完成 // 100ms后输出 >>> 触发fetchUser
可以调用cancel(task),来取消 task 任务。这个和setIntervalclearInterval接口设计相似。
那么 fork、spawan 有啥区别呢?
这里借用操作系统的进程概念,fork 出来的任务会阻塞父任务;spawan 出来的任务不会阻塞父任务,同理,也不受父任务取消的影响。

事件处理:take、takeEvery、takeLatest

takeEvery、takeLatest 的区别好理解,就是响应 action.type,触发回调函数。
它们和 take 的区别呢?take 可以主动地等待用户操作;takeEvery 和 takeLatest 是被动的收到消息。
例如登录和登出的代码,用 take 可以写成:
function* loginFlow() { while (true) { const { user, password } = yield take("LOGIN_REQUEST"); // fork return a Task object 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")); } }
如果用 takeLatest,则写成:
function* watchLoginRequest() { yield takeLatest("LOGIN_REQUEST", function* (action) { // 进行登录 }); } function* watchLoginError() { yield takeLatest("LOGIN_ERROR", function* (action) { // 登录出错 }); } function* watchLogout() { yield takeLatest("LOGOUT", function* (action) { // 登出 }); }

参考