1 什么是副作用
副作用代指与redux app 之外的世界进行的所有交互,多数是与服务器或本地存储进行的交互。
那么我们在哪里处理副作用呢?你已了解到,只有当reducer是纯函数时才能发挥redux的优势,并且组件应dispatch acton创建器。
2 回顾thunk
你已拥有使用action创建器和中间件处理副作用的经验。当需要执行异步操作时,redux-thunk使得action创建器可返回一个函数来替代action。在action创建器fetchTasks中,返回了一个匿名函数thunk,thunk中间件提供了dispatch和getState参数,因此在函数内可访问当前store内容并dispatch新的acton。
3 saga介绍
saga提供了一种强有力的方式,可编写并合理组织复杂的异步行为。saga可使异步代码拥有与同步代码一致的可读性。
saga不执行或解决副作用,而只是返回如何处理副作用的描述。执行权则留给了里面的中间件。因此测试saga很简单。可测试saga是否返回正确的副作用描述,而无须模拟store。
4 实现saga
saga作为中间件运行,且中间件在创建store时进行注册。
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';
const sagaMiddleWare = createSagaMiddleware();
const store = createStore(
reducer,
composeWithDevTools(applyMiddeware(thunk, sagaMiddleware))
)
sagaMiddleware.run(rootSaga);
src/sagas.js
export default function* rootSaga(){
console.log('rootSaga reporting for duty')
}
根saga的作用就是协调APP中使用的所有其他saga。根saga实现后,它将启动其他saga并在后台运行,监听并响应特定类型的action。
saga有时被称为监听者,因为它们无限地等待并监听特定action。组织多个监听者的一种常见范式是使用fork将它们形成分支。
src/sagas.js
import {fork} from 'redux-saga/effects';
export function* rootSaga(){
yield fork(watchFetchTasks);
yield fork(watchSomethingElse);
}
function* watchFetchTasks(){
...
}
function* watchSomethingElse(){
...
}
fork
fork处理了什么呢?当执行根saga时,fork会在每个yield语句处暂停,直至副作用完成。而fork允许根saga前进到下一个yield语句,而无须解决当前分支监听者。每一个分支应该都是非阻塞的。这种实现是有意义的,因为我们期望在初始化时启动所有监听者,而非仅仅启动列表中的第一个。
fork是可用于辅助器管理所谓effect方法之一
take
take方法用于在接收到特定action时唤起并组合saga。与fork不同,这是阻塞调用,意味着无限循环将暂停以等待另一个类型为FETCH_TASKS_STARTED的action到达。
put
put 用于派发新的action,将期望传递的action以参数的方式,传递给中间件的其余部分和reducer。
import {call, fork, put, take} from 'redux-saga/effects';
//...
function* watchFetchTasks(){
while(true){
yield take(FETCH_TASKS_STARTED);
try{
const {data} = yiele call(api.fetchTasks);
yield put({
type: FETCH_TASKS_SUCCEEDED,
payload: {tasks: data}
})
}catch(e){
yield put({
type: FETCH_TASKS_FAILED,
payload: {error: e.message}
})
}
}
}
takeLatest
takeLatest:在希望在接收新请求时,取消未完成的旧同类请求使用。taskLatest可替换根saga中的fork方法。
import {call, put, takeLatest} from 'redux-saga/effects';
export function* rootSaga(){
yield takeLatest(FETCH_TASKS_STARTED, fetchTasks);
}
function* fetchTasks(){
//...
}
takeLates方法在后台创建了一个具有额外功能的分支。
5 处理长时间运行的进程
当期望限制对远程服务器的API请求的使用频次时,takeLatest方法很有意义。但是有时也期望放开所有请求限制,需要使用的方法是takeEvery。
delay
delay 是阻塞方法,应用为saga将暂停,直至delay方法被决议。 src/sagas.js
import {delay} from 'redux-saga';
import {call, put, takeEvery, takeLatest} from 'redux-saga/effects';
export default function* rootSaga(){
yield takeLatest(FETCH_TASK_STARTED, fetchTasks);
yield takeEvery(TIMER_STARTED, handleProgressTimer);
}
// 多个独立的计时器
function* handleProgressTimer({payload}){
while(true){
yield call(delay, 1000);
yield put({
type: TIMER_INCREMENT,
paylpad:{ taskId: paylpad.taskId},
})
}
}
上面代码每秒会派发一个TIMER_INCREMENT action。
使用通道
takeEvery为收到的每个TIMER_STARTE类型的action启动一个新进程。这正是期望的。
然而,TIMER_STOPPED action是由handleProgressTimer saga接收处理的,它也会启动一个新进程,该进程独立于处理计算器递增的进程。如无法追踪特定的递增进程,则无法停止该进程。
如果使用takeLatest 而非 takeEvery,则每次仅有一个任务递增其计时器的值。
我们期望启动一个进程,就需要支持停止该进程。
为了实现些功能,需要使用另一个redux-saga工具 - 通道channel。
我们期望为每个启动了计时器的任务创建唯一的通道。如保留一个通道列表,那么当任务需停止时,可将TIMER_STOPPED action发数转正确通道。
可通过创建一个辅助函数来管理这些通道,taskeLatestById是一个通用的辅助函数,用于创建可重新发现的进程。该函数检查某任务是否存在通道,如果不存在,则创建一个通道并将其添加转映射中。添加至映射后,立即实例化该通道,并派发action转新通道。
import {channel, delay} from 'redux-saga';
import {call, put, take, takeLatest} from 'redux-saga/effects';
//...
export default function* rootSaga(){
yield takeLatest(FETCH_TASKS_STARETED, fetchTasks);
yeild takeLatestById(TIMER_STARTED, handleProgressTimer);
}
function* takeLatestById(actionType, saga){
const channelsMap = {};
while(true){
const action = yield take(actionType);
const {taskId} = action.payload;
if(!channelsMap[taskId]){
channelsMap[taskId] = channel();
// 如果任务中不存在通道,则创建一个
yield takeLatest(channelsMap[taskId], saga);
//为任务创建新进程
}
yield put(channelsMap[taskId], action);
//派发一个action转特定进程
}
}