在React/Redux应用中使用Sagas管理异步操作(翻译)

在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

我们可以把这个app可以看做一个在stopped和Running两个状态之间相互转变的有限状态机(finite machine).参见下面的简图.当timer在Running状态时,状态机会每一秒种更新app一次.

状态机

让我首先把app的基本设置看一下,然后我们演示一下怎么在action creators和React组件之外使用sagas帮助管理异步操作(side-effects).

Actions

在模块中有四个actions

  1. START-计时器改变为运行状态.
  2. TICK-时钟每个滴答以后递增定时器
  3. STOP-计时器改变为停止状态
  4. 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中,有两种方法可以处理周期性的更新.

  1. 视图周期性的回调action creator
  2. 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是和系统交互的永久线程:

  1. 对系统中的acion dispach做出反应
  2. 往系统中Dispatch新的actions
  3. 可以使用内部机制在没有外部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了吗?好了,有什么想法呢?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容