在React/Redux應(yīng)用中使用Sagas管理異步操作(翻譯)

在React/Redux應(yīng)用中使用Sagas管理異步操作

參考這篇文章,譯文可能先幼稚算色,參考看看吧.redux-saga本身的一些概念很難
可以也參考 redux-saga的文檔

Redux是一個和Flux類似的框架,在React社區(qū)中增長很快.他通過使用單個狀態(tài)的原子性和純函數(shù)式reduce來更新state,從而加強單向數(shù)據(jù)流,減小了數(shù)據(jù)操作的復(fù)雜性.
對于我,配置React+Flux一直是一根肉中刺,包括有Action creators的協(xié)作,異步操作也是非常的棘手.解決辦法是在React組件中使用生命周期方法(life cycle),例如componentDidupdate,componentWillUpdate等等,在action creators中通過返回thunks(類似Promise對象)對象也可以工作.但是這些方法似乎在有些條件下會不太好使用.
我了更好的表達(dá)我的意思挫以,我們來看看一個簡單的Timer App. 整個APP的代碼可在這里.

計時器APP

這個app允許使用者開始和停止一個定時器,也可以重置它.

計時器-app

我們可以把這個app可以看做一個在stopped和Running兩個狀態(tài)之間相互轉(zhuǎn)變的有限狀態(tài)機(finite machine).參見下面的簡圖.當(dāng)timer在Running狀態(tài)時,狀態(tài)機會每一秒種更新app一次.

狀態(tài)機

讓我首先把app的基本設(shè)置看一下,然后我們演示一下怎么在action creators和React組件之外使用sagas幫助管理異步操作(side-effects).

Actions

在模塊中有四個actions

  1. START-計時器改變?yōu)檫\行狀態(tài).
  2. TICK-時鐘每個滴答以后遞增定時器
  3. STOP-計時器改變?yōu)橥V範(fàn)顟B(tài)
  4. RESET-復(fù)位定時器
  // actions.js,四種action

export default { start: () => ({ type: 'START' })
               , tick: () => ({ type: 'TICK' })
               , stop: () => ({ type: 'STOP' })
               , reset: () => ({ type: 'RESET' })
               };

狀態(tài)模型和Reducer

計時器的狀態(tài)由兩部分屬性組成:status和seconds

 type Model = {
  status: string;
  seconds: number;
 }

status是運行和停止兩個狀態(tài),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).視圖渲染當(dāng)前的時間和狀態(tài).于此同時在用戶點擊Reset,Start或Stop按鈕的時候喚醒相應(yīng)的回調(diào)函數(shù).

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的狀態(tài)是在運行和停止之間轉(zhuǎn)變,但是還沒有周期性改變定時器的機制.
在典型的Redux+React的app中,有兩種方法可以處理周期性的更新.

  1. 視圖周期性的回調(diào)action creator
  2. action creator返回一個thunk對象,這個對象周期性的dispatch TICK actions.
解決方案1:讓視圖dispatch更新

對于#1方案,視圖必須等待定時器的狀態(tài)從停止轉(zhuǎn)變?yōu)殚_始才能開始周期性的action派發(fā).意思是我們不得不使用有狀態(tài)的組件.

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);
  }

  // ...
}

這種處理方式可以工作,但是這會使視圖變得滿是狀態(tài),而且也會不純凈.另一個問題是我們的組件現(xiàn)在不僅僅需要渲染HTML,捕獲用戶的交互操作還要承擔(dān)更多的工作.這種方式里引入致異步操作會使視圖和應(yīng)用作為一個整體桩了,很難理清.在計時器這個app里面可能還不是什么問題.但是如果在一個大型的應(yīng)用中附帽,你可能想把異步操作放到整個應(yīng)用的外面.

所以使用Thunks對象怎么樣?

解決方案2:在Action Creator中使用Thunks對象

替代方案1在視圖中進(jìn)行操作,可以在我們的action creator中使用thunks.改變一下start的action creator

 export default {
  start: () => (
    (dispatch, getState) => {
      // This transitions state to Running
      dispatch({ type: 'START' });
      //上面的注釋的譯文:dispatch({type:'START'})改變狀態(tài)為Running

      // Check every 1 second if we are still Running.
      // If so, then dispatch a `TICK`, otherwise stop
      // the timer.
      //每一秒種檢測一下狀態(tài)是不是還是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回調(diào)函數(shù)被調(diào)用.接著只要計時器只要還在工作井誉,每一秒鐘將會dispatch一個TICK action.
在action creator中使用的方式一個問題是action creator現(xiàn)在要做很多的事情.測試也是一個很難完成的任務(wù),因為沒有返回任何數(shù)據(jù).

最好的解決辦法是:使用Sagas去管理計時器.

redux-sagas重新定義side-effects為Effects.Effects由Sagas生成.sagas的概念據(jù)我所知來自CQRS和Event Sourcing世界.有許多討論爭論sagas到底是什么,但是你可以認(rèn)為sagas是和系統(tǒng)交互的永久線程:

  1. 對系統(tǒng)中的acion dispach做出反應(yīng)
  2. 往系統(tǒng)中Dispatch新的actions
  3. 可以使用內(nèi)部機制在沒有外部actions的情況下自我復(fù)蘇.例如周期性的蘇醒.

在redux-saga里,一個saga就是一個生成器函數(shù)(generator function),可以在系統(tǒng)內(nèi)無限期運行.當(dāng)特定action被dispatch時,saga就可以被喚醒.saga也可以繼續(xù)dispatch額外的actions,也可以接入程序的單一狀態(tài)樹.
例如,我們想在計時器運行的時候,周期性的dispatch TICKS.看看下面的操作:

  function* runTimer(getState) {
  // The sagasMiddleware will start running this generator.
  //sagas中間件將開始運行這個生成器函數(shù).
  
  

  // Wake up when user starts timer.
  //當(dāng)用戶開始計時器的時候喚醒.
  while(yield take('START')) {
    while(true) {
      // This side effect is not run yet, so it can be treated
      //side effect 沒有運行,所以可以看做數(shù)據(jù)
      // 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.
      //如果計時器沒有運行的話,就進(jìn)入休眠狀態(tài)等待計時器的重新工作
      } else {
        break;
      }
    }
  }
}

正如你所見到的,一個saga使用普通的JavaScript控制流程來構(gòu)建協(xié)作side-effects和action creators的過程.take函數(shù)在START action被dispatch的時候喚醒.call函數(shù)允許我們創(chuàng)建類似于待辦事項的等待效果.(就是類似list-todo,已經(jīng)在日程表中列出,但還沒有執(zhí)行的任務(wù))
通過使用saga,我們可以保持視圖和action creator成為純函數(shù).saga使我們可以使用類似javascript構(gòu)造函數(shù)的方式創(chuàng)建state轉(zhuǎn)變的模型.

包裝

Sagas是系統(tǒng)內(nèi)管理side-effects的途徑.當(dāng)你的應(yīng)用中需要長時間運行的進(jìn)程來協(xié)作多個action creators和side-effects的時候,Sagas將會非常的合適.
Sagas不僅對actions做出響應(yīng),而且對內(nèi)部機制也可以做出響應(yīng)(例如,時間依賴的effects).Sagas尤其有用蕉扮,特別是你需要在正常的Flux流程之外管理side-effects的時候.例如,一個用戶的交互操作可能會有更多的action產(chǎn)生,但是這些actions卻不需要用戶更多的操作.
最后,當(dāng)你需要一個無限狀態(tài)機模型的時候,sagas也值得一試.

如果你想看看Timer app的完整代碼,看看這里.

你準(zhǔn)備嘗試sagas了嗎?好了,有什么想法呢颗圣?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喳钟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子在岂,更是在濱河造成了極大的恐慌奔则,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔽午,死亡現(xiàn)場離奇詭異易茬,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進(jìn)店門抽莱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來范抓,“玉大人,你說我怎么就攤上這事食铐∝暗妫” “怎么了?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵虐呻,是天一觀的道長象泵。 經(jīng)常有香客問我,道長铃慷,這世上最難降的妖魔是什么单芜? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮犁柜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘堂淡。我一直安慰自己馋缅,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布绢淀。 她就那樣靜靜地躺著萤悴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪皆的。 梳的紋絲不亂的頭發(fā)上覆履,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天,我揣著相機與錄音费薄,去河邊找鬼硝全。 笑死,一個胖子當(dāng)著我的面吹牛楞抡,可吹牛的內(nèi)容都是我干的伟众。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼召廷,長吁一口氣:“原來是場噩夢啊……” “哼凳厢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起竞慢,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤先紫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后筹煮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遮精,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年寺谤,在試婚紗的時候發(fā)現(xiàn)自己被綠了仑鸥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吮播。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖眼俊,靈堂內(nèi)的尸體忽然破棺而出意狠,到底是詐尸還是另有隱情,我是刑警寧澤疮胖,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布环戈,位于F島的核電站,受9級特大地震影響澎灸,放射性物質(zhì)發(fā)生泄漏院塞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一性昭、第九天 我趴在偏房一處隱蔽的房頂上張望拦止。 院中可真熱鬧,春花似錦糜颠、人聲如沸汹族。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽顶瞒。三九已至,卻和暖如春元旬,著一層夾襖步出監(jiān)牢的瞬間榴徐,已是汗流浹背葡秒。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工盖淡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晚顷,地道東北人序厉。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓扣囊,卻偏偏與公主長得像秃臣,于是被迫代替她去往敵國和親沸枯。 傳聞我的和親對象是個殘疾皇子肤视,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,781評論 2 361

推薦閱讀更多精彩內(nèi)容