淺談React數據流管理


 

引言:為什么數據流管理如此重要?react的核心思想就是:UI=render(data)data就是我們說的數據流,renderreact提供的純函數,所以用戶界面的展示完全取決於數據層。這篇文章希望能用最淺顯易懂的話,將react中的數據流管理,從自身到借助第三方庫,將這些概念理清楚。我會列舉幾個當下最熱的庫,包括它們的思想以及優缺點,適用於哪些業務場景。這篇文章不是教程,不會講如何去使用它們,更不會一言不合就搬源碼,正如文章標題所說,只是淺談,希望讀者在讀完以后就算原先沒有使用過這些庫,也能大致有個思路,知道該如何選擇性地深入學習。

 

在本文正式開始之前,我先試圖講清楚兩個概念,狀態和數據:

我們都知道,react是利用可復用的組件來構建界面的,組件本質上是一個有限狀態機,它能夠記住當前所處的狀態,並且能夠根據不同的狀態變化做出不同的操作。在react中,把這種狀態定義為state,用來描述該組件對應的當前交互界面,表示當前界面展示的一種狀況,react正是通過管理狀態來實現對組件的管理,當state發生變更時,react會自動去執行相應的操作:繪制界面。

所以狀態是針對react component這種有限狀態機才出現的名詞,而數據就廣泛了,它不光是指server層返回給前端的數據,react中的狀態也是一種數據,當我們改變數據的同時,就要通過改變狀態去引發界面的變更;我們真正要關心的是數據層的管理,我們今天所討論的數據流管理方案,特別是后面介紹的幾種第三方庫,不光是配合react,也可以配合其他的View框架(VueAngular等等),就好比開頭提到的那個公式,引申一下:UI = X(data),但今天主要是圍繞react來講的,因此我們在說react的狀態管理其實和數據流管理是一樣的,包括我們會借助第三方庫來幫助react管理狀態,希望不要有小伙伴太糾結於此。

 

一,react自身的數據流管理方案

我們先來回顧一下,react自身是如何管理數據流的(也可以理解為如何管理應用狀態):

 

react是自上而下的單向組件數據流,容器組件&展示組件(也叫傻瓜組件&聰明組件)是最常用的react組件設計方案,容器組件負責處理復雜的業務邏輯以及數據,展示組件負責處理UI層,通常我們會將展示組件抽出來進行復用或者組件庫的封裝,容器組件自身通過state來管理狀態,setState更新狀態,從而更新UI,通過props將自身的state傳遞給展示組件實現通信。

 

這是當業務需求不復雜,頁面較簡單時我們常用的數據流處理方式,僅用react自身提供的propsstate來管理足矣,但是如果稍微增加一點復雜度呢,比如當我們項目中遇到這些問題:

 

1,如何實現跨組件通信、狀態同步以及狀態共享?

react V16.3以前,通過狀態提升至最近的共同父組件來實現。(雖然有官方提供的context API,但是舊版本存在一個問題:看似跨組件,實則還是逐級傳遞,如果中間組件使用了ShouldComponentUpdate檢測到當前stateprops沒有變化,return false,那么context就會無法透傳,因此context沒有被官方推薦使用)。

 

react V16.3版本以后,新版本context解決了之前的問題,可以輕松實現,但依然存在一個問題,context也是將底部子組件的狀態控制交給到了頂級組件,但是頂級組件狀態更新的時候一定會觸發所有子組件的re-render,那么也會帶來損耗。(雖然我們可以通過一些手段來減少重繪,比如在中間組件的SCU里進行一些判斷,但是當項目較大時,我們需要花太多的精力去做這件事)

 

 

2,如何避免組件臃腫?

當某個組件的業務邏輯非常復雜時,我們會發現代碼越寫越多,因為我們只能在組件內部去控制數據流,沒辦法抽離,ModelView都放在了View層,整個組件顯得臃腫不堪,業務邏輯統統堆在一塊,難以維護。

 

3,如何讓狀態變得可預知,甚至可回溯?

當數據流混亂時,我們一個執行動作可能會觸發一系列的setState,我們如何能夠讓整個數據流變得可監控,甚至可以更細致地去控制每一步數據或狀態的變更?

 

4,如何處理異步數據流?

react自身並未提供多種處理異步數據流管理的方案,僅用一個setState已經很難滿足一些復雜的異步流場景;

 

如何改進?

這個時候,我們可能需要一個真正的數據流管理工具來幫助react了,我們希望它是真正脫離react組件的概念的,從UI層完全抽離出來,只負責管理數據,讓react只專注於View層的繪制,那這也是為什么我們需要使用那些第三方數據流管理工具的原因,接下來我們就來了解一些當前社區比較熱門的數據流管理工具。

 

二,redux

我直接跳過了flux來說redux,主要是因為redux是由flux演變而來,可以說是flux的升級加強版,flux具備的優勢redux也做到了。

 

redux提供了哪些?

 

1store:提供了一個全局的store變量,用來存儲我們希望從組件內部抽離出去的那些公用的狀態;

2action:提供了一個普通對象,用來記錄我們每一次的狀態變更,可日志打印與調試回溯,並且這是唯一的途徑;

3reducer:提供了一個純函數,用來計算狀態的變更;

 

為什么需要redux

很多人在用了一段時間的redux之后,最大的感想就是,redux要寫大量的模板代碼,很麻煩,還不如只用react來管理。特別是在react的新context推出以后,許多人更是直接棄用了redux,甚至覺得redux已死。如果說舊版的context的弊端,我們通過redux配合react-redux來實現跨組件的狀態通信同步等問題,那確實新版本的context可以替換掉這個功能點,但如果你的項目中僅僅是用redux做這些,那思考一下,你是否真的需要redux?也許從一開始你就不需要它。(雖然新版的context功能強大,但是依然是通過一個新的容器組件來替我們管理狀態,那么通過組件管理狀態的問題依舊會存在,Consumer是和Provider一一對應的,在項目復雜度較高時,可能會出現多個Provider,更多個Consumer,甚至會一個Consumer需要對應多個Provider的傳值等一系列復雜的情況,所以我們依然要謹慎使用)

 

redux的核心競爭力

1,狀態持久化:global store可以保證組件就算銷毀了也依然保留之前狀態;

2,狀態可回溯:每個action都會被序列化,Reducer不會修改原有狀態,總是返回新狀態,方便做狀態回溯;

3Functional Programming:使用純函數,輸出完全依賴輸入,沒有任何副作用;

4,中間件:針對異步數據流,提供了類express中間件的模式,社區也出現了一大批優秀的第三方插件,能夠更精細地控制數據的流動,對復雜的業務場景起到了緩沖地作用;

 

 

與其說是redux來幫助react管理狀態,不如說是將react的部分狀態移交至redux那里,redux徹頭徹尾的純函數理念就表明了它不會參與任何狀態變化,完全是由react自己來完成,只不過redux會提供一套工具,react照着說明書來操作罷了,所以這注定了想要接受redux,就必須按照它的規矩來做,除非你不願意接受這種FP的模式。這種模式有利有弊,有利就是在一個大型的多人團隊中,這種開發模式反而容易形成一種規約,讓整個狀態流程變得清晰,弊端就是對於小規模團隊,尤其是着急發布上線的,這種繁重的代碼模板無疑是一種負擔。

 

redux的缺點:

1,繁重的代碼模板:修改一個state可能要動四五個文件,可謂牽一發而動全身;

2store里狀態殘留:多組件共用store里某個狀態時要注意初始化清空問題;

3,無腦的發布訂閱:每次dispatch一個action都會遍歷所有的reducer,重新計算connect,這無疑是一種損耗;

4,交互頻繁時會有卡頓:如果store較大時,且頻繁地修改store,會明顯看到頁面卡頓;

5,不支持typescript

 

關於如何優化,網上有很多優秀的案例,redux官方也提供了很多方法,這里不再贅述。redux未來不會有太大的變化,那些弊端還是會繼續保留,但是這依然不會妨礙忠愛它的用戶去使用它。

 

如果說redux那種強硬的函數式編程模式讓很多人難以接受,那么當他們開始mobx的使用的時候,無疑是一種解脫。

 

三,mobx

最開始接觸mobx也是因為redux作者Dan Abramov的那句:Unhappy with reduxtry mobx,我相信很多人也是因為這句話而開始了解學習並使用它的。

 

下面列舉一些mobx的優勢(和redux進行一個對比)

1redux不允許直接修改state,而mobx可隨意修改;

2redux修改狀態必須走一套指定的流程較麻煩,而mobx可以在任何地方直接修改(非嚴格模式下);

3redux模板代碼文件多,而mobx非常簡潔,就一個文件;

4redux只有一個storestate or store難以取舍,而mobxstore,你可以把所有的state都放入store中,完全交給mobx來管理,減少顧慮;

5redux需要對監聽的組件做SCU優化,減少重復render;而mobx都是Smart Component,不用我們手動做SCU

 

mobx的設計思想:

 

說了這么多,如果你是第一次了解mobx,是不是聽着就感覺很爽!沒錯,這就是mobx的魅力,那它是如何實現這些功能的呢?這里以mobx 5版本為例,實際上它是利用了ES6proxy來追蹤屬性(舊版本是用Object.defineProperty來實現的)通過隱式訂閱,自動追蹤被監聽的對象變化,然后觸發組件的UI更新;如果說redux是把要做的事情都交給了用戶,來保證自己的純凈,那么mobx就是把最簡易的操作給了用戶,其它的交給mobx內部去實現,用戶不必關心這個過程,ModelView完全分離,我們完全可以將業務邏輯寫在action里,用戶只需要操作Observable data就行了,Observer view會自動做出響應,這就是mobx主打的響應式設計,但是編程風格依然是傳統的面向對象的OO范式。(熟悉Vue的朋友一定對這種響應式設計不陌生,Vue就是利用了數據劫持來實現雙向綁定,其實React + Mobx就是一個復雜點的VueVue 3版本一個重大改變也是將代理交給了proxy

 

剛剛mobx的優勢說得比較多了,這邊再總結一下:

1,代碼量少;

2,基於數據劫持來實現精准定位(真正意義上的局部更新);

3,多store抽離業務邏輯(Model View分離);

4,響應式性能良好(頻繁的交互依然可以勝任);

5,完全可以替代react自身的狀態管理;

6,支持typescript

 

但是mobx真的這么完美嗎,當然也有缺陷:

1,沒有狀態回溯能力:mobx是直接修改對象引用,所以很難去做狀態回溯;(這點redux的優勢就瞬間體現出來了)

2,沒有中間件:和redux一樣,mobx也沒有很好地辦法處理異步數據流,沒辦法更精細地去控制數據流動;(redux雖然自己不做,但是它提供了applyMiddleware!)

3store太多:隨着store數的增多,維護成本也會增加,而且多store之間的數據共享以及相互引用也會容易出錯

4,副作用:mobx直接修改數據,和函數式編程模式強調的純函數相反,這也導致了數據的很多未知性

 

其實現在主流的數據流管理分為兩大派,一類是以redux為首的函數式庫,還有一類是以mobx為首的響應式庫,其實通過剛剛的介紹,我們會發現,reduxmobx有一個共同的短板,那就是在處理異步數據流的時候,沒有一個很好的解決方案,至少僅僅依靠自身比較吃力,那么接下來給大家介紹一個處理異步數據流的高手:rxjs

 

四,rxjs

我相信很多人聽說過rxjs學習曲線異常陡峭,是的,除了眼花繚亂的各類操作符(目前rxjs V6版本有120+個),關鍵是它要求我們在處理事務的時候要貫徹一切皆為流的理念,更是讓初學者難以理解。這一小節並不能讓讀者達到能夠上手使用的程度,正如文章開頭所說,希望讀者(新手)能對rxjs有一個新的認知,知道它是做什么的,它是如何實現的,它有哪些優勢,我們如何選擇它,如果感興趣還需要私下花大量時間去學習掌握各種操作符,但我也會嘗試盡可能地相對於前兩個說得更細致一些。

 

在開始介紹rxjs之前,我們先來簡單地聊聊什么是響應式編程?我以一個很簡單的小例子來看:a + b = c;如果站在傳統的命令式編程的角度來看這段公式:c的值完全依賴於ab,這時候我們去改變a的值,那我們就需要再去手動計算a + b的值,abc是相互依賴的;那么如果站在響應式編程的角度來看,這個公式又會變成這樣:

c := a + bab完全不關心c的值,c也完全不關心等式那邊是a或者b,或者還有什么def。。。等式右邊改變值了,左邊會自動更改數值,這就是響應式編程的思維方式。我們再來看前端的框架歷史,傳統命令式編程的代表:jQuery,在過去我們是如何繪制一個頁面的?我們會用jQuery提供的一套API,然后手動操作Dom來進行繪制,很精准,但是很累,因為完全靠手動操作,且改動時性能損耗較大,開發者的注意力完全在如何去繪制上面了;那我們再來看響應式編程的react,它是如何來實現的?開發者根本不用關心界面如何繪制,只要告訴react我們希望頁面長什么樣子,就可以了,剩下的交給reactreact就會自動幫我們繪制界面,還記得開頭時的那個核心思想嗎:UI = render(data),我們只要操作data就可以了,頁面UI會自動作出響應,而且我們一切的操作都是基於內存之中,不會有較大的性能損耗,這就是react響應式編程的精髓,也是為何它叫作react

 

回到我們的rxjs上,rxjs是如何做到響應式的呢?多虧了它兩種強大的設計模式:觀察者模式和迭代器模式;簡單地介紹一下:

1,觀察者模式:

 

在觀察者模式中,有兩個重要的角色:ObservableObserver,熟悉mobx的同學對這個一定不陌生(所以我建議想要學習rxjs的同學,如果對mobx不熟悉,可以先學習一下mobx,然后再學習rxjs,這樣會更容易理解一些),就是可觀察對象和觀察者,可觀察對象(Observable)也就是事件發布者,負責產生事件,而觀察者(Observer)也就是事件響應者,負責對發布的事件作出響應,但是如何連接一個發布者和響應者呢?通過訂閱的形式,也就是subscribe方法(這也類似於reduxstore.subscribe),而在訂閱之前,他們兩者是毫無關聯的,無論Observable發出多少事件,Observer也不會做出任何響應,同樣,當這種訂閱關系中斷時也不會;

 

2,迭代器模式:

在這里要先引出一個新的概念:拉取(pull)和推送(push),rxjs官方這兩種協議有更詳細的解釋,我這里就直接引用一下: 

 

拉取和推送實際上對於觀察者來說就是一個主動與被動的區別,是主動去獲取還是被動地接收。在rxjs中,作為事件響應者(消費者)的Observer對象也有一個next屬性(回調函數),用來接收從發布者那里過來的數據。(站在開發者的角度,我們一定是希望消息是被動地接收,因為我們倡導的就是通過操作data數據層,讓View層進行一個響應,那么這里data數據層一定是事件發布者,而View層就是事件響應者,每當data數據層發生變化時,都會主動推送一個值給View層,這才符合真正意義上的響應式編程,而rxjs做到了!)

 

如何配合react

如果說reduxmobx的出現或多或少是因為react的存在,那么不同的是rxjsreact並沒有什么關聯,關於rxjs的歷史這里不多說,感興趣的可以了解一下Reactive Extensionrxjs只是響應式編程在JavaScript中的應用。那么如何幫助react實現狀態管理呢,我們只需要將組件作為事件響應者,然后在next回調里定義好更新組件狀態的動作setState,當接收到數據推送時,就會自動觸發setState,完成界面更新,這其實有點類似於mobx做的事情。(很多人在react項目中並沒有完全只使用rxjs,而是用了這個redux-observable中間件,利用rxjs的操作符來處理異步action

 

 

除了響應式編程的魅力,rxjs還有什么優勢呢?

1,純函數:rxjs中數據流動的過程中,不會改變已經存在的Observable實例,會返回一個新的Observable,沒有任何副作用;

2,強大的操作符:rxjs又被稱為lodash for async,和lodash一樣,擁有眾多強大的操作符來操作數據流,不光是同步數據,特別是針對各種復雜的異步數據流,甚至可以多種事件流組合搭配,匯總到一起處理;

3,更獨立:rxjs並不依賴於任何一個框架,它可以任意搭配,因為它的關注點完全就是在於數據流的處理上,而且它更偏底層一些

 

rxjs有哪些缺點呢?

1,學習曲線陡峭:光是這一點就已經讓大多數人止步於此;

2,事件流高度抽象:用rxjs的用戶反饋一般都是兩種極端情況,用得好的都覺得這個太厲害了,用得不好的都覺得感覺有點麻煩,增加了項目復雜度。

 

最后,總結一下各類的適用場景:

1,當我們項目中復雜程度較低時,建議只用react就可以了;

2,當我們項目中跨組件通信、數據流同步等情況較多時,建議搭配react的新context api

3,當項目復雜度一般時,小規模團隊或開發周期較短時,建議使用mobx

4,當項目復雜度較高時,團隊規模較大或要求對事件分發處理可監控可回溯時,建議使用redux

5,當項目復雜度較高,且數據流混雜時,建議使用rxjs

 

結語:其實回顧全篇,我沒有提到一個關鍵點是,各個庫的性能對比如何。其實它們之間一定是有差異的,但是這點性能差異,相對於react自身組件設計不當而導致的性能損耗來說,是可以忽略的,如果你現在的項目覺得性能較差或者頁面卡頓,建議先從react層面去考慮如何進行優化,然后再去考慮如何優化數據管理層。關於上面提到的三個數據流管理工具,有利有弊,針對弊端,網上也有一大批優秀的解決方案和改進,感興趣的讀者可自行查閱。

 

謝謝!

 

=============

ps:這篇文章會發布在攜程公眾號上,不是抄襲,我就是作者,先更新到博客園上而已。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM