redux-saga 是做什么?
redux-saga 基于 yield 语法,能够创建+管理更加复杂的异步操作。
比如有时候点击前端按钮,发起异步请求。为了防止频繁点击请求,需要进行节流防抖。除了可以在发起请求的时候,进行节流防抖。还可以把节流防抖的时机提前,在状态更新的时候节流防抖。
怎么理解呢?就是一定时间内的点击,只考虑最近一次的点击。最近这次点击才会触发回调函数,发起异步,更新状态。
redux-saga 就提供了很多这样的操作符来控制更复杂的异步流程。
redux-saga 的简单使用
先看代码,假设要更新用户状态:
sagas.js
文件,定义 saga:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
const { put, takeLatest, delay, call } = require("redux-saga/effects");
function* fetchUser(userId) { yield delay(100); return { sex: 1, userId, }; }
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); 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
const { watchUserFetchRequested } = require("./sagas"); const { createStore, applyMiddleware } = require("redux");
const createSagaMiddleware = require("redux-saga").default; const sagaMiddleware = createSagaMiddleware();
function startUserReq(state = {}, action) { switch (action.type) { 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; } }
const store = createStore(startUserReq, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(watchUserFetchRequested);
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 点的顺序,上述代码的输出是:
1 2 3 4
| watch action.type: USER_FETCH_REQUESTED reducer USER_FETCH_REQUESTED invoke action.type: USER_FETCH_REQUESTED reducer USER_FETCH_SUCCEEDED
|
对于第 4 点,代码换成以下的样子,就会死循环:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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); 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
操作符。
1 2 3 4
| const [users, repos] = yield all([ call(fetchUser, { role: 'user' }), call(fetchStudent, { role: 'student' }) ])
|
effects/all
和Promise.all
的行为类似,effects/race
和Promise.race
的行为类似。
异步任务:fork、spawan
前面yield call(...)
是阻塞的,等待 call 中的异步任务完成后,才会向下执行。
如果想异步执行,那么需要使用fork(...)
,返回异步标识,然后通过effects/cancel
来取消。
上面的sagas.js
改造下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function* fetchUser(userId) { yield delay(100); console.log(">>> 触发fetchUser");
if (yield cancelled()) { }
return { sex: 1, userId, }; }
function* watchUserFetchRequested() { yield takeLatest("USER_FETCH_REQUESTED", function* (action) { try { const task = yield fork(fetchUser, action.payload); console.log(">>> fork完成"); yield cancel(task); yield put({ type: "USER_FETCH_SUCCEEDED", payload: userInfo }); } catch (error) { yield put({ type: "USER_FETCH_FAILED", payload: error.message }); } }); }
|
上面代码输出是:
1 2 3
| >>> fork完成 // 100ms后输出 >>> 触发fetchUser
|
可以调用cancel(task)
,来取消 task 任务。这个和setInterval
、clearInterval
接口设计相似。
那么 fork、spawan 有啥区别呢?
这里借用操作系统的进程概念,fork 出来的任务会阻塞父任务;spawan 出来的任务不会阻塞父任务,同理,也不受父任务取消的影响。
事件处理:take、takeEvery、takeLatest
takeEvery、takeLatest 的区别好理解,就是响应 action.type,触发回调函数。
它们和 take 的区别呢?take 可以主动地等待用户操作;takeEvery 和 takeLatest 是被动的收到消息。
例如登录和登出的代码,用 take 可以写成:
1 2 3 4 5 6 7 8 9 10
| 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")); } }
|
如果用 takeLatest,则写成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function* watchLoginRequest() { yield takeLatest("LOGIN_REQUEST", function* (action) { }); }
function* watchLoginError() { yield takeLatest("LOGIN_ERROR", function* (action) { }); }
function* watchLogout() { yield takeLatest("LOGOUT", function* (action) { }); }
|
参考