Redux 整合笔记三 Saga

206 阅读4分钟

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转特定进程
    }
}