在React/Redux应用中使用Sagas管理异步操作
参考这篇文章,译文可能先幼稚,参考看看吧.redux-saga本身的一些概念很难
可以也参考 redux-saga的文档
Redux是一个和Flux类似的框架,在React社区中增长很快.他通过使用单个状态的原子性和纯函数式reduce来更新state,从而加强单向数据流,减小了数据操作的复杂性.
对于我,配置React+Flux一直是一根肉中刺,包括有Action creators的协作,异步操作也是非常的棘手.解决办法是在React组件中使用生命周期方法(life cycle),例如componentDidupdate,componentWillUpdate等等,在action creators中通过返回thunks(类似Promise对象)对象也可以工作.但是这些方法似乎在有些条件下会不太好使用.
我了更好的表达我的意思,我们来看看一个简单的Timer App. 整个APP的代码可在这里.
计时器APP
这个app允许使用者开始和停止一个定时器,也可以重置它.
我们可以把这个app可以看做一个在stopped和Running两个状态之间相互转变的有限状态机(finite machine).参见下面的简图.当timer在Running状态时,状态机会每一秒种更新app一次.
让我首先把app的基本设置看一下,然后我们演示一下怎么在action creators和React组件之外使用sagas帮助管理异步操作(side-effects).
Actions
在模块中有四个actions
- START-计时器改变为运行状态.
- TICK-时钟每个滴答以后递增定时器
- STOP-计时器改变为停止状态
- RESET-复位定时器
// actions.js,四种action
export default { start: () => ({ type: 'START' })
, tick: () => ({ type: 'TICK' })
, stop: () => ({ type: 'STOP' })
, reset: () => ({ type: 'RESET' })
};
状态模型和Reducer
计时器的状态由两部分属性组成:status和seconds
type Model = {
status: string;
seconds: number;
}
status是运行和停止两个状态,seconds只要定时器开始计时就开始累积.
Reducer的实际代码如下
// reducer.js
const INITIAL_STATE = { status: 'Stopped'
, seconds: 0
};
export default (state = INITIAL_STATE, action = null) => {
switch (action.type) {
case 'START':
return { ...state, status: 'Running' };
case 'STOP':
return { ...state, status: 'Stopped' };
case 'TICK':
return { ...state, seconds: state.seconds + 1 };
case 'RESET':
return { ...state, seconds: 0 };
default:
return state;
}
};
Timer的UI视图
视图(view)是比较单纯的的,所以和异步操作是完全隔绝的(side-effects free).视图渲染当前的时间和状态.于此同时在用户点击Reset,Start或Stop按钮的时候唤醒相应的回调函数.
export const Timer = ({ start, stop, reset, state }) => (
<div>
<p>
{ getFormattedTime(state) } ({ state.status })
</p>
<button
disabled={state.status === 'Running'}
onClick={() => reset()}>
Reset
</button>
<button
disabled={state.status === 'Running'}
onClick={() => start()}>
Start
</button>
<button
disabled={state.status === 'Stopped'}
onClick={stop}>
Stop
</button>
</div>
);
问题:怎么处理周期性的更新操作.
目前app的状态是在运行和停止之间转变,但是还没有周期性改变定时器的机制.
在典型的Redux+React的app中,有两种方法可以处理周期性的更新.
- 视图周期性的回调action creator
- action creator返回一个thunk对象,这个对象周期性的dispatch TICK actions.
解决方案1:让视图dispatch更新
对于#1方案,视图必须等待定时器的状态从停止转变为开始才能开始周期性的action派发.意思是我们不得不使用有状态的组件.
class Timer extends Component {
componentWillReceiveProps(nextProps) {
const { state: { status: currStatus } } = this.props;
const { state: { status: nextStatus } } = nextProps;
if (currState === 'Stopped' && nextState === 'Running') {
this._startTimer();
} else if (currState === 'Running' && nextState === 'Stopped') {
this._stopTimer();
}
}
_startTimer() {
this._intervalId = setInterval(() => {
this.props.tick();
}, 1000);
}
_stopTimer() {
clearInterval(this._intervalId);
}
// ...
}
这种处理方式可以工作,但是这会使视图变得满是状态,而且也会不纯净.另一个问题是我们的组件现在不仅仅需要渲染HTML,捕获用户的交互操作还要承担更多的工作.这种方式里引入致异步操作会使视图和应用作为一个整体,很难理清.在计时器这个app里面可能还不是什么问题.但是如果在一个大型的应用中,你可能想把异步操作放到整个应用的外面.
所以使用Thunks对象怎么样?
解决方案2:在Action Creator中使用Thunks对象
替代方案1在视图中进行操作,可以在我们的action creator中使用thunks.改变一下start的action creator
export default {
start: () => (
(dispatch, getState) => {
// This transitions state to Running
dispatch({ type: 'START' });
//上面的注释的译文:dispatch({type:'START'})改变状态为Running
// Check every 1 second if we are still Running.
// If so, then dispatch a `TICK`, otherwise stop
// the timer.
//每一秒种检测一下状态是不是还是Running,如果是的
//话,dispatch ‘TICK’aciton.否则就停止计时器
const intervalId = setInterval(() => {
const { status } = getState();
if (status === 'Running') {
dispatch({ type: 'TICK' });
} else {
clearInterval(intervalId);
}
}, 1000);
}
)
// ...
};
Start action creator将会dispatch一个START action,只要start回调函数被调用.接着只要计时器只要还在工作,每一秒钟将会dispatch一个TICK action.
在action creator中使用的方式一个问题是action creator现在要做很多的事情.测试也是一个很难完成的任务,因为没有返回任何数据.
最好的解决办法是:使用Sagas去管理计时器.
redux-sagas重新定义side-effects为Effects
.Effects
由Sagas生成.sagas的概念据我所知来自CQRS和Event Sourcing世界.有许多讨论争论sagas到底是什么,但是你可以认为sagas是和系统交互的永久线程:
- 对系统中的acion dispach做出反应
- 往系统中Dispatch新的actions
- 可以使用内部机制在没有外部actions的情况下自我复苏.例如周期性的苏醒.
在redux-saga里,一个saga就是一个生成器函数(generator function
),可以在系统内无限期运行.当特定action被dispatch时,saga就可以被唤醒.saga也可以继续dispatch额外的actions,也可以接入程序的单一状态树.
例如,我们想在计时器运行的时候,周期性的dispatch
TICKS
.看看下面的操作:
function* runTimer(getState) {
// The sagasMiddleware will start running this generator.
//sagas中间件将开始运行这个生成器函数.
// Wake up when user starts timer.
//当用户开始计时器的时候唤醒.
while(yield take('START')) {
while(true) {
// This side effect is not run yet, so it can be treated
//side effect 没有运行,所以可以看做数据
// as data, making it easier to test if needed.
//这样测试比较容易一点
yield call(wait, ONE_SECOND);
// Check if the timer is still running.
//检测计时器是否运行
// If so, then dispatch a TICK.
//如果计时器运行的话,就dispatch一个TICK
if (getState().status === 'Running') {
yield put(actions.tick());
// Otherwise, go idle until user starts the timer again.
//如果计时器没有运行的话,就进入休眠状态等待计时器的重新工作
} else {
break;
}
}
}
}
正如你所见到的,一个saga使用普通的JavaScript控制流程来构建协作side-effects和action creators的过程.take函数在START
action被dispatch的时候唤醒.call函数允许我们创建类似于待办事项的等待效果.(就是类似list-todo,已经在日程表中列出,但还没有执行的任务)
通过使用saga,我们可以保持视图和action creator成为纯函数.saga使我们可以使用类似javascript构造函数的方式创建state转变的模型.
包装
Sagas是系统内管理side-effects的途径.当你的应用中需要长时间运行的进程来协作多个action creators和side-effects的时候,Sagas将会非常的合适.
Sagas不仅对actions做出响应,而且对内部机制也可以做出响应(例如,时间依赖的effects).Sagas尤其有用,特别是你需要在正常的Flux流程之外管理side-effects的时候.例如,一个用户的交互操作可能会有更多的action产生,但是这些actions却不需要用户更多的操作.
最后,当你需要一个无限状态机模型的时候,sagas也值得一试.
如果你想看看Timer app的完整代码,看看这里.
你准备尝试sagas了吗?好了,有什么想法呢?