單頁應用的資料流方案探索

發表於2017-04-19

大家好,現在是2017年4月。過去的3年裡,前端開發領域可謂風起雲湧,革故鼎新。除了開發語言的語法增強和工具體系的提升之外,大部分人開始習慣幾件事:

  • 元件化
  • MDV(Model Driven View)

所謂元件化,很容易理解,把檢視按照功能,切分為若干基本單元,所得的東西就可以稱為元件,而元件又可以一級一級組合而成複合元件,從而在整個應用的規模上,形成一棵倒置的元件樹。這種方法論歷史久遠,其實現方式或有瑜亮,理念則大同小異。

而MDV,則是對很多低階DOM操作的簡化,把對DOM的手動修改遮蔽了,通過從資料到檢視的一個對映關係,達到了只要運算元據,就能改變檢視的效果。

Model-Driven-View

給定一個資料模型,可以得到對應的的檢視,這一過程可以表達為:

其中的f就是從Model到View的對映關係,在不同的框架中,實現方式有差異,整體理念則是類似的。

當資料模型產生變化的時候,其對應的檢視也會隨之變化:

另外一個方面,如果從變更的角度去解讀Model,資料模型不是無緣無故變化的,它是由某個操作引起的,我們也可以得出另外一個表示式:

把每次的變更綜合起來,可以得到對整個應用狀態的表達:

這個表示式的含義是:在初始狀態上,依次疊加後續的變更,所得的就是當前狀態。這就是當前最流行的資料流方案Redux的核心理念。

從整體來說,使用Redux,相當於把整個應用都實現為命令模式,一切變動都由命令驅動。

Reactive Programming 庫簡介

在傳統的程式設計實踐中,我們可以:

  • 複用一種資料
  • 複用一個函式
  • 複用一組資料和函式的集合

但是,很難做到:提供一種會持續變化的資料讓其他模組複用。

而一些基於Reactive Programming的庫可以提供一種能力,把資料包裝成可持續變更、可觀測的型別,供後續使用,這種庫包括:RxJS,xstream,most.js等等。

對資料的包裝過程類似如下:

這段程式碼中的a$、arr$、interval$都是一種可觀測的資料包裝,如果對它們進行訂閱,就可以收到所有產生的變更。

我們可以把這種封裝結構視為資料管道,在這種管道上,可以新增統一的處理規則,這種規則會作用在管道中的每個資料上,並且形成新的管道:

管道可被連續拼接,並形成新的管道。

需要注意的是:

  • 管道是懶執行的。一個拼接起來的資料管道,只有最末端被訂閱的時候,附加在管道上的所有邏輯才會被執行。
  • 一般情況下,管道的執行過程可以被共享,比如b$和c$兩個管道,都從a$變形得出,它們就共享了a$之前的所有執行過程。

也可以把多個管道組合在一起形成新的管道:

從這個關係中可以看出,當user$或task$中的資料發生變更的時候,priv$都會自動計算出最新結果。

在業務開發的過程中,可以使用資料流的理念,把很多東西提高一個抽象等級:

比如上面這個例子,統一處理了一個普通請求過程中的三種狀態:請求前、成功、異常,並且把它們的資料:loading、正常資料、異常資料都統一成一種,檢視直接訂閱處理就行了。

高度抽象的資料來源

很多時候,我們進行業務開發,都是在一種比較低層次的抽象維度上,在低層抽象上,存在著太多的冗餘過程。如果能夠對資料的來源和去向做一些歸納會怎樣呢?

比如說,從實體的角度,很可能一份資料初始狀態有多個來源:

  • 應用的預設配置
  • HTTP請求
  • 本地儲存
  • …等等

也很可能有多個事件都是在修改同一個東西:

  • 使用者從檢視發起的操作
  • 來自WebSocket的推送訊息
  • 來自Worker的處理訊息
  • 來自其它窗體的postMessage呼叫
  • …等等

如果不做歸納,可能會寫出包含以上各種東西的邏輯組合。若干個類似的操作,在過濾掉額外資訊之後,可能都是一樣的。從應用狀態的角度,我們不會需要關心一個資料究竟是從哪裡來的,也不會需要關心是通過什麼東西發起的修改。

用傳統的Redux寫法,可能會提取出一些公共方法:

基於方法呼叫的邏輯不能很好地展示一份資料的生命週期,它可能有哪些來源?可能被什麼修改?它是經過幾千年怎樣的辛苦修煉之後才能夠化成人形,跟你坐在一張桌子上喝咖啡?

我們可以藉助RxJS或者xstream這樣的庫,以資料管道的理念,把這些東西更加直觀地組織在一起:

初始狀態來源

資料變更過程的統一

在這樣的機制裡,我們可以很清楚地看到一塊資料的來龍去脈,它最初是哪裡來的,後來可能會被誰修改過。所有這樣的資料都放置在管道中,除了指定的入口,不會有其他東西能夠修改這些資料,檢視可以很安全地訂閱他們。

基於Reactive理念的這些資料流庫,一般是沒有針對業務開發的強約束的,也以直接訂閱並設定元件狀態,也可以拿它按照Redux的理念來使用,豐儉由人。

簡單的使用

類似Redux的使用方式

元件與外接狀態

我們前面提到,元件樹是一個樹形結構。理想中的元件化,是所有檢視狀態全部內建在元件中,一級一級傳遞。只有這樣,才能達到元件的最佳可複用狀態,並且,元件可以放心把自己該做的事情都做了。

但事實上,元件樹的層級可能很多,這會導致傳遞層級很多,很繁瑣,而且,存在一個經典問題,那就是兄弟元件,或者是位於元件樹的不同樹枝上的元件之間的通訊很麻煩,必須通過共同的最近的祖先節點去轉發。

像Redux這樣的機制,把狀態的持有和更新外接,然後通過connect這樣的方法,去把特定元件所需的外部狀態從props設定進去,但它不僅僅是一個轉發器。

我們可以看到如下事實:

  • 轉發器在元件樹之外
  • 部分資料在元件樹之外
  • 對這部分資料的修改過程在元件樹之外
  • 修改完資料之後,通知元件樹更新

所以:

  • 元件可以通過中轉器修改其他元件的狀態
  • 元件可以通過中轉器修改自身的狀態
  • 元件可以通過中轉器修改全域性的其他狀態

這樣看來,可以通過中轉器修改應用中的一切狀態。那麼,如果所有狀態都可以通過中轉器修改,是否意味著都應當通過它修改?

這個問題很大程度上等價於:

元件是否應當擁有自己的內部狀態?

我們可能會有如下的選擇:

  • 一切狀態外接,元件不管理自己狀態
  • 部分內建,由元件自己管理,另外一些由全域性Store管理

這兩種方式,在傳統軟體開發領域分別稱為貧血元件、充血元件,它們的差別是:元件究竟是純展示,還是帶一些邏輯。

也可以拿蟻群和人群來形容這兩種元件實踐。單個螞蟻的智慧程度很低,但它可以接受蟻王的指令去做某些事情,所有的麻煩事情都集中在上層,決策層的事務非常繁瑣。而人類則不同,每個人都有自己的思考和執行能力,一個管理有序的體系中,管理者只需決定他和自己直接下屬所需要做的事情就可以了。

在React體系中,純展示元件可被簡化為這樣的形式:

顯而易見,這種元件的優勢在於它的展示結果只跟輸入資料有關,所有狀態外接,因此,在熱替換等方面,可以做到極致。

然而,一旦這個元件複雜起來,自帶互動,可能就需要在事件、生命週期上做文章,免不了會需要一些中間狀態來表達元件自身的形態。

我們當然可以把這種狀態也外接,但這麼做有幾個問題:

  • 這樣的狀態只跟某元件自己有關,放出去到全域性Store,會增加Store的不必要的複雜度
  • 元件的自身形態狀態被外接,將導致元件與狀態的距離變遠,從而對這些狀態的讀寫變得比原先繁瑣
  • 帶互動的元件,無法獨立、完整地描述自身的行為,必須藉助外部管理器

如果是一種單獨提供的元件庫,比如像Ant Design這樣的,卻要依賴一個外部的狀態管理器,這是很不合適的,它會導致元件庫帶有傾向性,從而對使用者造成困擾。

總的來說,狀態全外接,元件退化為貧血元件這種實踐,可以得到不少好處,但代價是比較大的。

You might not need Redux這篇文章中,Redux的作者Dan Abramov提到:

Local State is Fine.

因此,我們就可能會面臨一個尷尬的狀況,在大部分實踐中:

一個元件的狀態,可能一半在元件內管理,一半在全域性的Store裡

以React為例,大致是這樣一個狀況:

我們看到,在render裡面,需要合併state和props的資料,但是在這裡做這個事情,是破壞了render函式的純潔性的。可是,除了這裡,別的地方也不太適合做這種合併,怎麼辦呢?

所以,我們需要一種機制,能夠把本地狀態和props在render之外統一起來,這可能就是很多實踐者傾向於把本地狀態也外接的最重要原因。

在React + Redux的實踐中,通常會使用connect對檢視元件包裝一層,變成一種叫做容器元件的東西,這個connect所做的事情就是把全域性狀態對映到元件的props中。

那麼,考慮如下程式碼:

我們是否可以把一個元件的內部狀態外接到被註釋掉的這個位置,然後也connect進來呢?這段程式碼其實是不起作用的,因為對localState的改變不會被檢測到,所以元件不會重新整理。

我們先探索這種模式是否可行,然後再來考慮實現的問題。

MVI架構

Plug and Play All Your Observable Streams With Cycle.js這篇文章中,我們可以看到一組理念:

  • 一切都是事件源
  • 使用Reactive的理念構建程式的骨架
  • 使用sink來定義應用的邏輯
  • 使用driver來隔離有副作用的行為(網路請求、DOM渲染)

基於這套理念,編寫程式碼的方式可以變得很簡潔流暢:

  • 從driver中獲取action
  • 把action對映成資料流
  • 處理資料流,並且渲染成介面
  • 從介面的事件中,派發action去進行後續事項的處理

在CycleJS的理念中,這種模式叫做MVI(Model View Intent)。在這套理念中,我們的應用可以分為三個部分:

  • Intent,負責從外部的輸入中,提取出所需資訊
  • Model,負責從Intent生成檢視展示所需的資料
  • View,負責根據檢視資料渲染檢視

整體結構可以這樣描述:

對比Redux這樣的機制,它的差異在於:

  • Intent實際上做的是action執行過程的高階抽象,提取了必要的資訊
  • Model做的是reducer的事情,把action的資訊轉換之後合併為狀態物件
  • View跟其他框架沒什麼區別,從狀態物件渲染成檢視。

此外,在CycleJS中,View是純展示,連事件監聽也不做,這部分監聽的工作放在Intent中去做。

我們可以從中發掘這麼一些東西:

  • View還是純渲染,接受的唯一引數就是一個表達檢視狀態的資料流
  • Model的返回結果就是上面那個流,不分內外狀態,全部合併起來
  • Model所合併的東西的來源,是從Intent中來的

對我們來說,這裡面最大關鍵在於:所有東西的輸入輸出都是資料流,甚至連檢視接受的引數、還有它的渲染結果也是一個流!奧祕就在這裡。

因此,我們只需在把待傳入檢視的props與檢視的state以流的方式合併,直接把合併之後的流的結果傳入檢視元件,就能達到我們在上一節中提出的需求。

元件化與分形

我們之前提到過一點,在一個應用中,元件是形成倒置的樹形結構的。當元件樹上的某一塊越來越複雜,我們就把它再拆開,延伸出新的樹枝和葉子,這個過程,與分形有異曲同工之妙。

然而,因為全域性狀態和本地狀態的分離,導致每一次分形,我們都要兼顧本元件、下級元件、全域性狀態、本地狀態,在它們之間作一些權衡,這是一個很麻煩的過程。在React的主流實踐中,一般可以利用connect這樣的高階函式,把全域性狀態對映進元件的props,轉化為本地狀態。

上一節提及的MVI結構,不僅僅能夠描述一個應用的執行過程,還可以單獨描述一個元件的執行過程。

所以,從整體來理解我們的應用,就是這樣一個關係:

這樣一直分形下去,每一級元件都可以擁有自己的View、Model、Intent。

狀態的變更過程

在模型驅動檢視這個理念下,檢視始終會是呼叫鏈的最後一段,它的職責就是消費已經計算好的資料,渲染出來。所以,從這個角度看,我們的重點工作在於怎麼管理狀態,包括結構的定義和變更的流轉過程。

Redux提供了對狀態定義和變更過程的管理思路,但有不少值得探討的地方。

基於標準Flux/Redux的實踐有一個共同點:繁瑣。產生這種繁瑣的最主要原因是,它們都是以自定義事件為核心的,自定義事件本身就是繁瑣的。由於收發事件通常位於兩個以上不相同的模組中,不得不以封裝的事件物件為通訊載體,並且必須顯式定義事件的key,否則接收方無法指定自己的響應。

一旦整個應用都是以此為基石,其中的繁瑣程度可想而知,所以社群會存在一些簡化action建立,或者通過約定來減少action收發中間環節的Redux周邊。

如果不從根本上對事件這種機制進行抽象,就不可能徹底解決繁瑣的問題,基於Reactive理念的這幾個庫天然就是為了處理對事件機制的抽象而出現的,所以用在這種場景下有奇效,能把action的派發與處理過程描述得優雅精妙。

注意一個問題,既然我們之前得到一種思路,把全域性狀態和本地狀態分開,然後合併注入元件,就需要考慮這樣的問題:如何管理本地狀態和全域性狀態,使用相同的方式去管理嗎?

在Redux體系中,我們在修改全域性狀態的時候,使用指定的action去修改狀態,原因是要區分那個哪個action修改state的什麼部分,怎樣修改。但是考慮本地狀態的情況,它反映的只是元件內部的資料變化,一般而言,其結構複雜程度遠遠低於全域性狀態,繼續採用這種方式的話並不划算。

Redux這類東西出現的初衷只是為了提供一種單向資料流的思路,防止狀態修改的混亂。但是在基於資料管道的這些庫中,資料天然就是單向流動的。在剛才那段程式碼裡,其實action的type是沒有意義的,一直就沒有用到。

實際上,這個程式碼中的updateActions$自身就表達了updateTodo的含義,而它後續的fold操作,實際上就是直接在reduce。理解了這一點之後,我們就可以寫出反映若干種資料變更的合集了,這個時候,可以根據不同的action去選擇不同的reducer操作:

我們注意到,這裡是把所有action全部merge了之後再fold的,這是符合Redux方式的做法。有沒有可能各自fold之後再merge呢?

其實是有可能的,我們只要能夠確保action導致的reducer粒度足夠小,比如只修改state的同一個部分,是可以按照這種維度去組織action的。

如果我們一個元件的內部狀態足夠簡單,甚至連action的型別都可以不需要,直接從操作對映到狀態結果。

這樣,我們可以在元件內執行這種簡化版的Redux機制,而在全域性狀態上執行比較完善的。這兩種都是基於資料管道的,然後在容器元件中可以把它們合併,傳入檢視元件。

整個流程如圖所示:

狀態的分組與管理

基於redux-saga的封裝庫dva提供了一種分類機制,可以把一類業務的東西進行分組:

從這個結構可以看出,這個在dva中被稱為model的東西,定義了:

  • 它是面向的什麼業務模型
  • 需要在全域性儲存什麼樣的資料結構
  • 經過哪些操作去變更資料

面向同一種業務實體的資料結構、業務邏輯可以組織到一起,這樣,對業務程式碼的維護是比較有利的。對一個大型應用來說,可以根據業務來劃分model。Vue技術棧的Vuex也是用類似的結構來進行業務歸類的,它們都是受elm的啟發而建立,因此會有類似結構。

回想到上一節,我們提到,如果若干個reducer修改的是state的不同位置,可以分別收斂之後,再進行合併。如果我們把狀態結構按照上面這種業務模型的方式進行管理,就可以採用這種機制來分別收斂。這樣,單個model內部就形成了一個閉環,能夠比較清晰的描述自身所代表的業務含義,也便於做測試等等。

MobX的Store就是類似這樣的一個組織形式:

依照之前的思路,我們所謂的model其實就是一個合併之後生成state結構的資料管道,因為我們的管道是可以組合的,所以沒有特別的必要去按照上面那種結構定義。

那麼,在整個應用的最上層,是否還有必要去做combineReducer這種操作呢?

我們之前提到一個表示式:

整個React-Redux體系,都是傾向於讓使用者儘可能去從整體的角度關注變化,比如說,Redux的輸入輸出結果是整個應用變更前後的完整狀態,React接受的是整個元件的完整狀態,然後,內部再去做diff。

我們需要注意到,為什麼不是直接把Redux接在React上,而是通過一個叫做react-redux的庫呢?因為它需要藉助這個庫,去從整體的state結構上檢出變化的部分,拿給對應的元件去重繪。

所以,我們發現如下事實:

  • 在觸發reducer的時候,我們是精確知道要修改state的什麼位置的
  • 合併完reducer之後,輸出結果是個完整state物件,已經不知道state的什麼位置被修改過了
  • 檢視元件必須精確地拿到變更的部分,才能排除無效的渲染

整個過程,是經歷了變更資訊的擁有——丟失——重新擁有過程的。如果我們的資料流是按照業務模型去分別建立的,我們可以不需要去做這個全合併的操作,而是根據需要,選擇合併其中一部分去進行運算。

這樣的話,整個變更過程都是精確的,減少了不必要的diff和快取。

如果為了使用redux-tool的話,可以全部合併起來,往redux-tool裡面寫入每次的全域性狀態變更資訊,供除錯使用,而因為資料管道是懶執行的,我們可以做到開發階段訂閱整個state,而執行時不訂閱,以減少不必要的合併開銷。

Model的結構

我們從巨集觀上對業務模型作了分類的組織,接下來就需要關注每種業務模型的資料管道上,資料格式應當如何管理了。

在Redux,Vuex這樣的實踐中,很多人都會有這樣的糾結:

在store中,應當以什麼樣的形式存放資料?

通常,會有兩種選擇:

  • 打平了的資料,儘可能以id這樣的key去索引
  • 貼近檢視的資料,比如樹形結構

前者有利於查詢和更新,而後者能夠直接給檢視使用。我們需要思考一個問題:

將處理過後的檢視狀態存放在store中是否合理?

我認為不應當存太偏向檢視結構的資料,理由如下:

某一種業務資料,很可能被不同的檢視使用,它們的結構未必一致,如果按照檢視的格式儲存,就要在store中存放不同形式的多份,它們之間的同步是個大問題,也會導致store嚴重膨脹,隨著應用規模的擴大,這個問題更加嚴重。

既然這樣,那就要解決從這種資料到檢視所需資料的關聯關係,這個處理過程放在哪裡合適呢?

在Redux和Vuex中,為了資料的變更受控,應當在reducer或者mutation中去做狀態變更,但這兩者修改的又是store,這又繞回去了:為了檢視渲染方便而計算出來的資料,如果在reducer或者mutation中做,還是得放在store裡。

所以,就有了一個結論:從原始資料到檢視資料的處理過程不應當放在reducer或mutation中,那很顯然就應當放在檢視元件的內部去做。

我們理一下這個關係:

這個圖中,方括號的部分是檢視元件,它內部包含了從原始state到view所需資料的變動,以React為例,用程式碼表示:

經過這樣的拆分之後,store中的結構更加簡單清晰,reducer的職責也更少了,檢視有更大的自主權,去從原始資料組裝成自己要的樣子。

在大型業務開發的過程中,store的結構應當儘早穩定無爭議,避免因為檢視的變化而不停調整,因此,存放相對原始一些的資料是更合理的,這樣也會避免檢視元件在理解資料上的歧義。多個檢視很可能以不同的業務含義去看待狀態樹上的同一個分支,這會造成很多麻煩。

我們期望在store中儲存更偏向於更扁平化的原始資料。即使是對於從後端返回的層級資料,也可以藉助normalizr這樣的輔助庫去展開。

展開前:

展開後:

很明顯,這樣的結構對我們的後續操作是比較便利的。因為我們手裡有資料管道這樣的利器,所以不擔心資料是比較原始的、離散的,因為對它們作聚合處理是比較容易的,所以可以放心地把這些資料打成比較原始的形態。

前端的資料建模

之前我們提到過store裡面存放的是扁平化的原始資料,但是需要注意到,同樣是扁平化,可能有像map那樣基於id作索引的,也可能有基於陣列形式存放的,很多時候,我們是兩種都要的。

在更復雜的情況下,還會需要有物件關係的關聯,一對一,一對多,多對多,這就導致檢視在需要使用store中的資料進行組合的時候,不管是store的結構定義還是組合操作都比較麻煩。

如果前端是單一業務模型,那我們按照前一節的方案,已經可以做到當資料變更的時候,把當前狀態推送給訂閱它的元件,但實際情況下,都會比這個複雜,業務模型之間會存在關聯關係,在一個模型變更的時候,可能需要自動觸發所關聯到的模型的更新。

如果複雜度較低,我們可以手動處理這種關聯,如果聯動關係非常複雜,可以考慮對資料按照實體、關係進行建模,甚至加入一個迷你版的類似ORM的庫來定義這種關係。

舉例來說:

  • 組織可以有下層組織
  • 組織下可以有人員
  • 組織和人員是一對多的關係

如果一個資料流訂閱了某個組織的基本資訊,它可能只反映這個組織自身實體上的變更,而另外一個資料流訂閱了該組織的全部資訊,用於形成一個實時更新的組織全檢視,則需要聚合該組織和可能的下級組織、人員的變動彙總。

上層檢視可以根據自己的需要,選擇從不同的資料流訂閱不同複雜度的資訊。在這種情況下,可以把整個ORM模組整體視為一個外部的資料來源。

整個流程如下:

這裡面有幾個需要注意的地方:

  • 一個action實際上還是對應到一個reducer,然後發起對state的更改,但因為state已經不是簡單結構了,所以我們不能直接改,而是通過這層類似ORM的關係去改。
  • 對ORM的一次修改,可能會產生對state的若干處改動,比如說,改了一個資料,可能會推匯出業務上與之有關係的一塊關聯資料的變更。
  • 如果是基於react-redux這樣基於diff的機制,同時修改state的多個位置是可以的,但在我們這套機制裡,因為沒有了先合併修改再diff的過程,所以很可能多個位置的修改需要通過ORM的關聯,延伸出不同的管道來。
  • 檢視訂閱的state變更,只能組合運算,不應當再幹別的事情了。

在這麼一種體系下,實際上前端存在著一個類似資料庫的機制,我們可以把每種資料的變動原子化,一次提交只更新單一型別的實體。這樣,我們相當於在前端部分做了一個讀寫分離,讀取的部分是被實時更新的,可以包含一種類似遊標的機制,供檢視元件訂閱。

下面是Redux-ORM的簡單示例,是不是很像在運算元據庫?

小結

文章最開始,我們提到最理想的元件化開發方式是依託元件樹的結構,每個元件完成自己內部事務的處理。當元件之間出現通訊需求的時候,不得不借助於Redux之類的庫來做轉發。

但是Redux的理念,又不僅僅是隻定位於做轉發,它更是期望能管理整個應用的狀態,這反過來對元件的實現,甚至應用的整體架構造成了較大的影響。

我們仍然會期望有一種機制,能夠像分形那樣進行開發,但又希望能夠避免狀態管理的混亂,因此,MVI這樣的模式某種程度上能夠滿足這種需求,並且達到邏輯上的自洽。

如果以MVI的理念來進行開發,它的一個元件其實是:資料模型、動作、檢視三者的集合,這麼一個MVI元件相當於React-Redux體系中,connect了store之後的高階元件。

因此,我們只需把傳統的元件作一些處理:

  • 檢視隔離,純化為展示元件
  • 內部狀態的定義清晰化
  • 描述出內部狀態的來源關係:state := actions.reduce(reducer, initState)
  • 將內部的動作以action的方式輸出到上面那個表示式關係中

這樣,元件就是自洽的一個東西,它不關注外面是不是Redux,有沒有全域性的store,每個元件自己內部執行著一個類似Redux的東西,這樣的一個元件可以更加容易與其他元件進行配合。

與Redux相比,這套機制的特點是:

  • 不需要顯式定義整個應用的state結構
  • 全域性狀態和本地狀態可以良好地統一起來
  • 可以存在非顯式的action,並且action可以不集中解析,而是分散執行
  • 可以存在非顯式的reducer,它附著在資料管道的運算中
  • 非同步操作先對映為資料,然後通過單向聯動關係組合計算出檢視狀態

回顧整個操作過程:

  • 資料的寫入部分,都是通過類似Redux的action去做
  • 資料的讀取部分,都是通過資料管道的組合訂閱去做

藉助RxJS或者xstream這樣的資料管道的理念,我們可以直觀地表達出資料的整個變更過程,也可以把多個資料流進行便捷的組合。如果使用Redux,正常情況下,需要引入至少一種非同步中介軟體,而RxJS因為自身就是為處理非同步操作而設計的,所以,只需用它控制好從非同步操作到同步的收斂,就可以達到Redux一樣的資料單向流動。如果想要在資料管道中接入一段承擔中介軟體職責的東西,也是非常容易的。

而RxJS、xstream所提供的資料流組合功能非常強大,天然提供了一切非同步操作的統一抽象,這一點是其他非同步方案很難相比的。

所以,這些庫,因為擁有下面這些特性,很適合做資料流控制:

  • 對事件的高度抽象
  • 同步和非同步的統一化處理
  • 資料變更的持續訂閱(訂閱模式)
  • 資料的連續變更(管道拼接)
  • 資料變更的的組合運算(管道組合)
  • 懶執行(無訂閱者,則不執行)
  • 快取的中間結果
  • 可重放的歷史記錄
    ……等等

相關文章