對使用Redux和Redux-saga管理狀態(tài)的思考

原文地址在我的博客, 轉(zhuǎn)載請注明出處,謝謝!

概述

本文介紹了對 Redux 狀態(tài)管理的思想、原理、架構(gòu)方法的認(rèn)識和思考以及配合redux-saga處理異步操作的實(shí)踐

前言

You know, React 只是屬于MV*架構(gòu)模式的 view 層,是一種狀態(tài)機(jī),只使用 React 難以控制大型、復(fù)雜的應(yīng)用,它需要一些框架來幫助管理狀態(tài),因此如何有效、簡單、易于測試地管理這個狀態(tài)機(jī)是各種架構(gòu)框架感興趣的。Facebook 早就意識到這個問題并提出了 Flux 架構(gòu),比較復(fù)雜; 后來出現(xiàn)了 Redux、Mobx 等。MobX 可以處理簡單數(shù)據(jù)流的場景,可以實(shí)現(xiàn)精確更新; Redux 是從 Flux 和其他框架借鑒了一些思想, 它比 Flux 簡單、易于理解、用于處理復(fù)雜數(shù)據(jù)流,并具有很強(qiáng)的擴(kuò)展性,社區(qū)誕生了像redux-thunk、redux-promise、redux-saga等中間件用于方便地處理異步操作。最近也在項目中使用了 Redux 及其中間件 redux-saga 來管理狀態(tài)和處理異步操作,這篇文章就來談?wù)勎覍λ鼈兊乃伎己蛯?shí)踐。

正文

Redux 思想

先來談?wù)劚尘埃ㄐ枨螅?/strong>

我覺得理解Redux的思想,談?wù)凪V*架構(gòu)模式的思路也許會有幫助。

MV*架構(gòu)模式,它們的核心都是職責(zé)分離、解耦,不同的層次做不同的事,能讓一個復(fù)雜、混亂的應(yīng)用變得思路清晰,代碼可以復(fù)用,并且易于測試,有利于分工合作,構(gòu)建更大、更復(fù)雜的應(yīng)用。

一個應(yīng)用要包括哪些功能?表現(xiàn)(view)、處理數(shù)據(jù)的邏輯(model)以及數(shù)據(jù)映射到表現(xiàn)層的邏輯(presenter or controller),數(shù)據(jù)在這三層之間流通(MVVM模式通過數(shù)據(jù)雙向綁定實(shí)現(xiàn)view和model同步)。

React 根據(jù)state來render,它只是個狀態(tài)機(jī),并沒有解決管理狀態(tài)的問題。我們在單純的使用React來寫組件的時候,經(jīng)常會遇到組件間通信和管理組件state的問題,前者常用的解決辦法就是把數(shù)據(jù)提到父組件共享;后者管理state簡單的情況還行,一復(fù)雜就很麻煩且容易出錯,再遇到一些需要異步處理的操作,想想就頭皮發(fā)麻。

當(dāng)你開發(fā)中遇到一些反人類的操作時,試著去想如何改變一下思路讓它變得更簡單,別耐著性子安慰自己開發(fā)就是這樣 :)

解決方案

Redux 正是用來解決大型React應(yīng)用所面臨的狀態(tài)管理、數(shù)據(jù)流通、異步處理、測試、團(tuán)隊合作等問題:

Redux 用單一的object tree來表示整個應(yīng)用的state,這個表示state的對象樹被放在唯一的store 中,state相當(dāng)于store的快照;所有組件都會通過API拿到這個state,各取所需;

Redux 把頁面上用戶的操作或者瀏覽器的行為(如路由的變化)定義為一個要更新state的action,這個action是一個普通對象,它包含了要執(zhí)行動作的類別和傳遞到state的數(shù)據(jù)(如果有的話),它只表明要更改state的意圖,相當(dāng)于一個信號,并不能直接修改state,Redux會集中處理這些信號,這個action由你來決定何時發(fā)起;

定義好信號,你還需要根據(jù)不同的信號定義不同的邏輯函數(shù)(reducers)來更新state。

通過這張圖來整理一下:

“redux 原理圖”

咳咳...比如用戶點(diǎn)擊的一個按鈕,你在按鈕上綁定的回調(diào)函數(shù)調(diào)用了一個(多個)action creator,action creator就返回了一個更新state局部數(shù)據(jù)的action,store會根據(jù)這個(多個)action找到對應(yīng)的reducers(reducer需要做拆分),按照action發(fā)起的順序依次執(zhí)行來更新state,每個reducer只負(fù)責(zé)更新自己關(guān)心那部分,根 reducer 把多個子 reducer 輸出合并成一個單一的 state 樹,生成一個新的state保存在store中,store中的state可以通過相應(yīng)API傳遞到子組件。

這就是整個數(shù)據(jù)流。

那Redux如何處理異步操作?

Redux借鑒了中間件思想,利用可擴(kuò)展的中間件來改造dispatch函數(shù)。比如redux-thunk讓dispatch不僅僅可以接收action,還可以接受函數(shù)作為參數(shù),你可以在這個函數(shù)里完成異步操作。再如redux-saga更強(qiáng)大、也更復(fù)雜,在后面會講到。

Redux 架構(gòu)方法

對于React技術(shù)棧,Redux實(shí)現(xiàn)了react-redux庫來讓Redux管理React應(yīng)用(其他框架也有相應(yīng)的庫),里面集成了一些有用的函數(shù)來把一些明確的流程自動化,如createStore用于創(chuàng)建唯一store,可以把根reducer傳進(jìn)createStore使store自動調(diào)用對應(yīng)reducer,可以擴(kuò)展中間件;提供<Provider store>組件和connect高階組件用來包裹render component并傳遞state,connect還能自動dispatch,讓你只要調(diào)用action creator就能dispatch;提供combineReducers來組合分割的reducers等。

知道這些特性,就可以配合react-router構(gòu)建大型應(yīng)用了:

總的思路就是:利用react-router 把應(yīng)用分割為各個頁面,reducer、action creator也跟隨頁面分割而分割。每個路由對應(yīng)的頁面下都有components和containers,分別存放functional components 和class components,前者用來渲染,后者當(dāng)做containers被connect包裹,containers包裹c(diǎn)omponents;containers從connect得到state并映射需要的數(shù)據(jù)到子組件的props,子組件再向下傳遞。

具體如何構(gòu)建React + Redux + react-router,我在另一篇博客里講了。

使用這種架構(gòu),開發(fā)大型應(yīng)用變得得心應(yīng)手。

Redux 存在的問題

但是當(dāng)我深入項目開發(fā)的時候,也逐漸發(fā)現(xiàn)了一些問題:

  • 這種架構(gòu)項目結(jié)構(gòu)不夠扁平化,文件嵌套比較深,思路比較復(fù)雜,搭建、寫起來比較麻煩,上手有難度;
  • 由于所有action creator都定義在頁面層次上,讓子組件調(diào)用必須一層一層的傳遞,很麻煩且非常容易出錯,也很難調(diào)試;
  • state難以做到局部更新(這個可以用reselect
  • Redux只是傳遞了一種思路,定義了幾個簡單的API,很靈活,架構(gòu)方式不固定,設(shè)計方式不固定(如:如何設(shè)計state樹)但這也是它的缺點(diǎn),新人往往看完一遍還是不知道怎么做,對新人不友好

總之,redux可以勝任復(fù)雜數(shù)據(jù)流的應(yīng)用,但是也比較難,前期架構(gòu)比較麻煩,適合有經(jīng)驗的人。

使用redux-saga處理異步操作

Redux 倡導(dǎo)action 和reducer盡可能"純凈",沒有什么“副作用”。可是像一些異步操作比如獲取數(shù)據(jù)是必須的,在哪處理這些副作用呢?redux 把這些"不純凈的"任務(wù)交給了中間件,通過 向createStore里應(yīng)用中間件,在交由store處理action之前就可以對其完成一些其他的操作:

“redux 中間件”

而redux-saga 是Redux一個強(qiáng)大但并不復(fù)雜的用于異步處理的中間件。

它的思路是什么?相比其他redux異步中間件如redux-thunk、redux-promise有什么不同?

先看名字來理解:saga,這個術(shù)語常用于CQRS架構(gòu),代表查詢與責(zé)任分離

沒錯,就是查詢(dispatch)與責(zé)任(sagas)分離。saga提供了action監(jiān)聽函數(shù),只需在組件里dispatch 相應(yīng)type的action,就可以自動調(diào)用你定義好的對應(yīng)這個action的異步處理函數(shù)(sagas)來完成任務(wù),保證了只在組件里dispatch action來發(fā)起異步操作而不是redux-thunk、redux-promise的調(diào)用action creators。

另外一大特色就是redux-saga做到了異步代碼以同步方式寫,非常直觀方便,怎么做到的呢?它是利用了ES6新魔法Generator迭代器,可以完美解決異步回調(diào)地獄,讓你以同步方式寫異步。saga正是利用Generator特性讓其處理異步變得非常方便又容易理解。這是一個常見的請求后臺數(shù)據(jù)的異步操作,感受一下:

function *fetchNodeDetailByNodeId({ payload: { nodeId } }, { call, put }) {
      try {
        const { data, status }= yield call(fetchNodeDetailByNodeId, nodeId)
        if (data && status.errmsg === 'success') {
          yield put({
            type: 'setStates',
            payload: {
              nodeDetailData: data,
            },
          });
        } else {
          message.info('開了個小差,再試一次吧..');
        }
      } catch (error) {
        console.log(error);
      }
    },

call 和 put 是saga的API,相當(dāng)于dispatch,但是并不是真正執(zhí)行dispatch,只是發(fā)送你指定的指令,交由saga中間件來執(zhí)行這個指令。這樣看來,這個saga函數(shù)就是一些指令的集合,稱為effects,副作用,用來描述任務(wù)

為啥要描述指令而不直接調(diào)用呢?這樣是因為易于測試,如果直接調(diào)用,你還得模擬調(diào)用的函數(shù),詳見redux-saga文檔。

我覺得redux-saga相比于其他中間件的優(yōu)點(diǎn):

  • 查詢與責(zé)任分離,保證了action的純潔性,符合redux設(shè)計思想
  • 實(shí)現(xiàn)以同步方式寫異步操作,容易理解,邏輯清晰
  • 通過發(fā)送指令而不是直接調(diào)用讓異步操作變得容易測試
  • 監(jiān)聽、執(zhí)行自動化
  • 提供了豐富強(qiáng)大的指令來完成復(fù)雜的操作,比如無阻塞調(diào)用,同時執(zhí)行多個任務(wù)等

講道理,任何redux異步操作都可以讓saga這個中間件來完成,非常復(fù)雜的同樣可以勝任,并且很容易理解(異步操作以同步方式寫)和測試。再配合dva,可以減輕redux的復(fù)雜度同時完成更強(qiáng)大的功能。

這樣以來,redux配合saga,就可以讓它們各司其職,整個思路也變得清晰起來:

redux 倡導(dǎo)action和reducer要純潔,那就讓所有異步操作這些不純潔的任務(wù)交給saga,reducer不用變,還是純函數(shù);定義好對應(yīng)action的sagas專門用來處理異步操作,我只要在組件需要的地方里dispatch 純action就行了,符合redux設(shè)計思想。

總結(jié)

使用redux來管理應(yīng)用狀態(tài)適用于復(fù)雜的應(yīng)用,而復(fù)雜的應(yīng)用會有復(fù)雜的異步處理,異步處理不要用redux的action creator,它不是用來做這個的,也違背了redux設(shè)計思想,redux把這些任務(wù)交給了異步中間件,應(yīng)該由它們來完成。使用redux saga是一個推薦的選擇,它懂redux,也懂你需要什么。另外,既然你用到了saga,不妨試試dva架構(gòu),5分鐘上手,值得一試。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,916評論 2 372

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