在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閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容