如何理解 Facebook 的 flux 應用架構?
官方主頁:Flux | Application Architecture for Building User Interfaces
中文翻譯:Flux應用架構大家是如何理解這個應用架構的,它與MVC, MV*架構的本質區別在哪裡呢?它實質上來源於哪些已有的編程思想或設計模式呢,是函數響應式編程(FRP)嗎?有哪些比較好關於flux的資料推薦閱讀?
我的理解,Flux 的核心就是一個簡單的約定:視圖層組件不允許直接修改應用狀態,只能觸發 action。應用的狀態必須獨立出來放到 store 裡面統一管理,通過偵聽 action 來執行具體的狀態操作。
所謂的單向數據流,就是當用戶進行操作的時候,會從組件發出一個 action,這個 action 流到 store 裡面,觸發 store 對狀態進行改動,然後 store 又觸發組件基於新的狀態重新渲染。
這樣做的好處:
1. 視圖組件變得很薄,只包含了渲染邏輯和觸發 action 這兩個職責,即所謂 "dumb components"。
2. 要理解一個 store 可能發生的狀態變化,只需要看它所註冊的 actions 回調就可以。
3. 任何狀態的變化都必須通過 action 觸發,而 action 又必須通過 dispatcher 走,所以整個應用的每一次狀態變化都會從同一個地方流過。其實 Flux 和傳統 MVC 最不一樣的就在這裡了。React 在宣傳的時候一直強調的一點就是 「理解你的應用的狀態變化是很困難的 (managing state changing over time is hard)」,Flux 的意義就在於強制讓所有的狀態變化都必須留下一筆記錄,這樣就可以利用這個來做各種 debug 工具、歷史回滾等等。
市面上各種各樣的 Flux 實現那麼多,歸根結底是因為 1. Flux 這個概念本來就定義鬆散,具體怎麼實現大家各有各的看法;2. 官方實現又臭又長,不好用。
最後,Flux 並不一定要配套 React,其思想在傳統的 MV* 體系中也完全可以應用。
---另,Flux 和 FRP 並沒有本質上的聯繫,倒是 Elm 和 Cycle.js 和 FRP 更有關係一點。不過前段時間 FRP 之父 Conal Elliot 剛說了現在這些新玩意兒都不是真正的 FRP...首先是核心回答:
單向數據流是Flux的核心理念,而其實對單向數據流的追求一直就存在,只是過去在實現View層的時候缺乏足夠強大的工具,這個理念在實施上有些困難。
我曾經在2012年底的一個項目當中使用了一種前端架構,就是任何UI不能直接對數據有寫操作。
當時我們是一個桌面端Hybrid應用,意味著幾乎所有的數據來源都是一堆Native介面,有同步取的、非同步取的、還有push的。設計這麼一個單向數據流的目的就是防止同一份數據有多個地方同時在寫,而還有一些邏輯會把一些UI狀態數據持久化到本地,UI規模一上來,整個亂亂亂。但是當時並沒有數據綁定和diff更新這麼先進的工具,於是用了一種很土鱉的辦法,就是Controller(我們當時叫為Business層)對數據的任何寫操作,都在事件匯流排上觸發事件,然後讓UI選擇性監聽,選擇性更新。
於是難度轉化為UI層為了提高性能,需要小心翼翼地對數據進行差異檢測。要麼就是Controller里對數據更新事件的不斷細化,造成事件數量變得很多,於是查事件文檔變成了家常便飯。反過來,UI用戶操作觸發了業務邏輯,就調用Business層的API,當API產生副作用的時候依然使用事件匯流排來觸發UI修改(而不允許UI通過API返回值/回調結果直接修改)。實際上這個單向數據流雖然造成了編碼的時候一定的困難,但整個數據的流向變得更清晰,而且因為UI自主產生的副作用非常少,而且都是不重要的,通常就是寫無關痛癢的彈出提示框,和主數據流沒什麼關係,這樣UI變得更加可測了——我們這個項目是和QA一起封閉開發的,由於Hybrid程序天生黑盒,QA負責人一開始就給我提出了可測性的要求。在向他證明了UI對數據層的只讀特性後,他開始放心的對Business層開始做黑盒單測(呃,本來應該是我們RD做的是吧),而把UI的測試直接放在很靠後的階段。
這個時候你會發現其實Backbone為代表的MVC結構也是有這個思路的,但還是那個問題,在M發生了變化之後,V需要解決一個小心翼翼的減少更新代價這個問題,這也是大家一直吐槽Backbone的V存在感稀薄的一個重要原因——缺乏一個有力的工具解決View更新的問題(你知道我要說的就是聲明式UI)。
OK,我們說回到Flux,它其實也是一個單向數據流。View對於數據層(store)完全的單向依賴,而View對於數據層的寫需求,則完全通過action分發(dispatch)的方式流回store進行操作。
在這裡UI對於store的依賴,理論上說,你要是不嫌麻煩,像我們過去那樣,自己diff自己修改UI不也行嗎?但是那樣就沒開發效率了。所以我們在這個地方引入先進的工具,比如immutable、tree-diff、數據綁定等等。
這樣做最大的好處依然是View對於數據層的只讀使得它是可預測(predictable)的——這個概念好像是angular里強調的吧,所以其實Angular也是能實現這樣的單向數據流的。
這也就意味著UI層具有更良好的可測性。說得高大上一點,就是「UI是數據的純函數」,再裝逼一點就是所謂「UI=f(state)」&不難發現,其實我們一直追求的就是這樣,就好比用Angular和React的Chrome DevTool插件的時候,其實非常關心的是VM數據,而非View本身,因為潛意識裡已經非常相信:既然這裡的View是聲明式的、是綁定的,那麼只要VM數據是對的,就有充分的理由相信View一定是對的(或者至少我們馬上就能明確BUG邊界在哪兒,更容易定位了)。
但我個人認為,如果讓UI真的太「純」(純函數的純),反而會降低開發效率。這就好比一些函數式語言為了提供副作用和狀態,又搞了Monad神馬這樣的工具。在進行組件化開發的時候,設定好組件黑白盒之間的界限,當以黑盒看待它時,它走單向數據流。當以白盒看待它時,你不必關心它內部是怎麼實現的(比如也許你要遷移一個你們過去寫的巨複雜的jQuery日曆組件進Angular來用)。
子曰:「每一個優雅的介面背後都有一個骯髒的實現。」Flux的單向數據流,其實也許只是我們長期以來一直理想的架構,只是過去沒有現在這麼先進的UI層工具來實現而已。而且現在實現聲明式View的工具非常的多,用React可以,Vue也可以,Angular也完全可以——這些工具不僅提供了聲明式的UI,還提供了良好結構化的組件開發模式,非常強大。在不需要實現組件化的情況下,用virtual-dom搭建一個單向數據流、最小化DOM更新的程序也是可以的啊。哇塞,沒想到關於 Flux 的問題竟然有 200+ 的人關注,React + Flux 的春天終於來了么!言歸正傳,我們團隊從去年7月開始正式在線上應用使用 React,目前所有線上業務已經全部 React 化。而 Flux,目前已經在部分頁面進行試點,也算是小有心得,下面就零碎的給大家分享一下。
Flux 不是框架,而提供了一套數據流動方案
這個概念在我的博客 http://undefinedblog.com/facebook-flux/ 中已經做出了說明,Flux 並不是一個「框架(Libaray)」,正如題干中提到的,它是一套「應用架構(Architecture)」。而這套架構解決的核心問題,就是數據在 React 應用中的流動方式及過程。
我們目前在線上使用的是 React + Backbone.Model 的 「MV」組合,即:Backbone 的 Model 提供數據源,React 的 Components 負責渲染,當 UI 事件觸發數據更新時,使用 Backbone.Model 的 set 方法 trigger change,然後在 React Components 的 componentDidMount 中監聽 Model 的 change,最新從 Model 中取出新的數據進行渲染。
說實話,這套「土方法」應付一個簡單的 Todo MVC 綽綽有餘,但是在複雜的頁面中就會出現這麼幾個致命的問題:
- Components 是有層級關係的,而每一個 Components 都能夠直接改變 Model 中的數據。當層級關係複雜後,你無法準確定位到到底是哪個 Component 改變了 Model。
- 當組件之間需要共享 Model 時,只能在共同的父級 Component 初始化 Model,並把 Model 當做 props 傳給子組件。這樣一方面增加了單個子組件載入時間,另一方面也違背了模塊設計的哲學,增加了模塊之間的耦合性。
- 當需要進行組件性能優化時(使用 shouldComponentUpdate 生命周期方法),若你組件用來渲染的數據既由 Model 提供、又需要根據新的 props 算出新的 state,這個生命周期的處理會十分複雜。
- ...
上面說了這麼多問題,下面就要講講 Flux 的優勢了。
上圖盜自我自己的博客,是 Flux 架構一個簡單的流程圖。
先看最上面,Controller-View 是整個應用的入口(常見如 app.js),它是一個 Root Component,在這個 Component 中拿到 Store 中的數據,當做 props 傳給子組件;同時,監聽 Store 的變化,一旦發生改變,則用 Store 中最新的數據重新 setState。
Controller-View 下面就是一個個的 React Component 了,它們和 Controller-View 最大的區別在於並不直接接觸 Store。用於渲染的數據來自 props,而需要改變數據時,如點擊了頁面上的一個按鈕需要將數據 + 1,則調用 Action 中的一個方法。
Action 是一個抽象概念,一個應用中可以有多個 Action。一個常見的 action 包含兩個部分,type 和 payload,type 用於指明當前的 action 是什麼 action(如 BUTTON_CLICK,或 DATA_LOADED) 等,通常是定義成常量的字元串;而 payload 則是這次 action 中包含的有效信息(這裡再打個廣告 http://undefinedblog.com/something-about-payload/)。
最後看 Store,上文我們講到一次 action 觸發後帶著 type 和 payload,最終會被 dispatch 給所有的 Store(題主對不起你,這裡好像終於扯到你想問的設計模式了。基於目前看到的應用,在使用同一個 dispatcher 的前提下,Flux 核心思想確實可以簡化成一個 EventBus,因為所有的 Store 都會響應每一個 action),每個 Store 會根據自己內部的邏輯判斷每一個 action 要不要響應。怎麼響應等。
貌似說著說著變成 Flux 科普文了,再拉回來,對比一下 Backbone.Model + React 和 Flux 的區別在於?- Component 中不直接改變數據!Component 中不直接改變數據!Component 中不直接改變數據!
這點很重要!當你的應用寫的日漸複雜的時候,你會因為 Backbone.Model 中的某個值到底是在哪一層的哪個 Component 中被改變而 debug 數小時不得解。反觀 Flux 抽象出了 Action 的概念,所有需要改變數據的操作都變成了調用某個 action。原來點擊按鈕數字 + 1,在 Backbone.Model 中就是 model.set("data", this.get("data") + 1);而在 Flux 中,變成了兩步:1、在 Component 中調用某個 action,形如 plusOne(); 2、在 action 中有一個 plusOne 方法,dispatch 一個事件,包含 type: BUTTON_CLICK,因為這裡只是簡單的 + 1邏輯,所以不再需要 payload。這樣看似把應用做複雜了,但是你能清楚的知道每一次操作究竟是哪個方法改變了哪個數據,debug 起來就會得心應手很多。
- 永遠不 mutate state props。團隊中有很多實習生在寫 React 應用的時候喜歡 http://this.state.xxx = xxx; ,了解 React 應用的同學都知道直接 mutate state 和 props 是標準的 anti-pattern。其實有時候,我自己也會不自知的 mutate props,比如某些 props 是對象數組,我只對數據進行一個淺拷貝就傳給一個可能 mutate props 的子組件,導致自己拿到的最終數據被 mutate,像這種問題也非常難 debug。這個問題 Flux 並沒有直接解決,但是你看到伴隨著 React + Flux 如雨後春筍般冒出的 Immutable 庫就知道這個方向是多麼的眾望所歸了。因為 Flux 中,所有的數據都儲存在 Store 中,你可以進行集中管理,因此將 Store 設計成 Immutable 就變得手到擒來了。就以目前最火的 Flux 變種 redux 為例(redux 大家有興趣的話也可以細聊一下),它的設計理念就是 Store 不再定義如何直接改變 state,而是如何 reduce state,也就是每一個 action 經由 Store 響應後,均會產生一個全新的 state。因而在 redux 中,也沒有了 Store 的概念,只剩 Reducers。
- 由於所有的數據都在 Store 中,對於非 Controller-View 的子組件來說,所有的數據均來自 props,因此在 shouldComponentUpdate 中,只需要對比 http://this.props.xxx !== http://nextProps.xxx 即可。一行代碼優化組件性能!
- 數據層(M)與視圖層(V)的抽離意味著更簡單的 Isomorphic/Universal 應用
- ...
本來列了一二三,但是說著說著就說跑題了貌似。為了呼應一下題主的問題,貼一些有用的參考資料吧:
- React 官方博客不能少,了解每次版本號變動帶來的升級是什麼 Blog | React
- awesome 系列,可以一看 enaqx/awesome-react · GitHub
- 火到沒朋友的 Flux 變種 redux(目前已拆分為 redux 及 react-redux,都跟著 React 學壞開始玩拆分了) gaearon/redux · GitHub
- 其實也不賴適合小應用的 Flux 變種 reflux reflux/refluxjs · GitHub
- Facebook 的 GraphQL 和 Relay 也可以關注一下
- 現有 Flux 變種的對比(已經不是最全的了) The State of Flux
- Flux 屆的 TodoMVC 對比大全 voronianski/flux-comparison · GitHub
最後想說兩句,現在市面上很多所謂的 Flux 框架,就是把 Dispatcher 進行封裝,把 Store 和 Controller-View 的監聽及接觸監聽關係隱藏了起來,其實也就是幾十行代碼的事兒(雖然我也想這麼干),真沒啥意思……
最後的最後打兩個廣告:
1. 國內第一本 ReactJS 中文教程已經上市,各大網上商城有售 http://book.douban.com/subject/26378583/2. 團隊誠招各路喜歡 React、Flux 的同學,一起喝最烈的酒,用最爽的技術棧!
恰好最近兩個項目都用了Flux的結構,都是用的Reflux,個人更加看好Redux,正好借題主寶地分享一些心得和看法。
首先,上面的 @尤雨溪@楊森 已經講得很好了。但在實踐中,由於官方對Flux的解釋過於鬆散,導致 經常能看到各種奇奇怪怪的Flux實踐。
這裡推薦一篇文章:
http://www.christianalfoni.com/articles/2015_08_02_Why-we-are-doing-MVC-and-FLUX-wrong視角很有意思,他的大部分觀點我也比較贊同,一句話的表述就是:
相比MV*, Flux其實更加貼近傳統MVC的設計思路。而隨著Redux這類single store 架構的出現,整個Flux社區正在繼續朝著傳統MVC的方向前行。
如果你是有過後端開發經驗的前端,那你一定會疑惑為什麼MVC架構在後端非常清晰,而前端MVC們卻如此脆弱?
後端MVC清晰的原因:
1. 系統只有一個輸入源,由Controller分發
2. 系統只有一個唯一 完整狀態(資料庫)如果把前端應用也看成一個 獨立的有狀態的系統,那前端MVC框架至少有兩個明顯問題:1. Controller被架空後端系統為響應HTTP請求而生,路由是天然而唯一的系統輸入口。但對一個前端系統而言,輸入至少有三類(按發生頻次排序):用戶操作、API返回、URL變更。多數前端MV*框架要麼C層只是簡單的路由,要麼乾脆取消了這一層,總之都沒有對系統的輸入作出完整的抽象與統一管理。 最後只能任由View或VM層直接與Model交互:|------| |--------| |-------|
| VIEW | ---&> | ROUTER | &<--&> | MODEL |
|------| |--------| |-------|
| ^
|------| | |
| VIEW | &<--------| |
|------| |
|
|------| |
| VIEW | &<------------------------|
|------| |
|
|------| |
| | &<------------------------|
| |
| | |-------|
| VIEW | &<--&> | MODEL |
| | |-------|
| |
| | |-------|
| | &<---&> | MODEL |
|------| |-------|
線框圖同樣來自:christianalfoni
2. 模型層只是server model的映射,並不能完全反映系統狀態。而事實上前端在API數據之外,有著大量只屬於展現層的數據或者說狀態,比如當前顯示的是哪個Tab,側邊信息欄要不要打開,它們也是前端系統狀態的一部分,卻不能被基於server model的模型層容納,最後只能流離在展現層,對整個前端系統而言,這意味著唯一完整狀態的缺失。
FLUX理念的提出, 最讓人眼前一亮的就是action和action creator層,官方最早版本的代碼只有一個dispatcher,足見其地位。
實現一個dispatcher不難,它的重要在於解決了上述兩個問題中的第一個:為系統的輸入 提供了統一的抽象與分發,如下圖:而相比傳統MVC的Server-oriented Model層,把API請求 前置,則讓Store有可能更純粹地面向前端,表徵前端系統的狀態,而不僅僅是當個Server數據池。
對前端應用來說,唯一輸入源有了、唯一狀態也有了雛形,雖然形式上與傳統MVC有所不同,但相比其它前端MVC的貌合神離,FLUX在核心理念上卻更為貼近。
題外話:
在FLUX中一個最常見的反模式就是在Store中發起API請求,由於這樣做的人不在少數,我初學之時也甚為困惑。現在看來繞過Action層更改系統狀態的行為和MV*並無區別(會導致不可預測的級聯更新),故個人將其歸入FLUX反模式行列
然而FLUX並非沒有問題:
一. 上面我們提到,對前端系統而言,輸入有三類,FLUX卻只規範了其中兩個,忽略了URL變化是的,這確實是一個問題。由於前端路由系統的複雜性,抽象並管理並非易事,而相對於User interaction和Server response,URL雖然也會導致系統狀態變更,其發生頻次和複雜度卻是最低的,個人更願意將其看作FLUX的一個取捨。
最近的rackt/redux-router · GitHub 就把url操作也納入了action和store,是個很好的方向。二. Store層依然含糊不清FLUX官方網站的提倡很理想:有狀態的組件越少越好,由上層Smart component持有狀態,狀態隨store變化,然後傳給下面的純組件們。然而FLUX官網上也有這麼一句:stores manage the application state for a particular domain within the application.
在這個思想指導下,通常大家還是習慣按領域模型(domain)拆分store。傳統MVC的問題2——非領域模型的應用狀態如何管理——並沒有得到很好的解決。
這裡有一篇文章討論這個問題:No Fit State: Managing UI state in Flux
回到問題的本源,什麼是 application state ?
todo list里的todo肯定是,那todo的編輯狀態是不是?一個彈出窗的開閉是不是?有一個長久以來的思維定勢:
通常前端認為的 應用狀態,是後端數據在前端的映射這句話本身不無道理, 宏觀角度看整個前端只是後端數據的展現層而已,然而隨著前端應用化,複雜度的增長 帶來了更多的無領域狀態,在很多項目中,已經到了要麼好好組織起來,要麼等著收bug的程度。
一個簡單的栗子
假設我們的需求為:有一個todo list,在彈出窗中編輯todo,回車時發出API請求,若請求成功則關閉彈窗,若失敗則彈出窗保留。如果按領域拆分store,那對應reflux和redux:1. reflux下可以放在View層,讓component去管,而這個開閉狀態本質上和 POST_TODO_COMPLETED 這個action是有關的,想要實現這個功能,就必須讓UI層直接監聽請求成功的Action。通常在reflux中我們會利用非同步action的triggerPromise,在view層onSubmit(todo) {
todoAction.post(todo).
then(/* close popup here */)
}
當然,這顯然違背了FLUX的 數據流,算是action和view的私相授受。
2. 在redux中,由於沒有triggerPromise這樣的「後門」,只能把狀態放進store,這樣當然更加FLUX,應用也變得更可預測。
可放進哪個domain下呢?彈窗是和頁面強相關的,domain則是和頁面無關的,這個矛盾很難調和:兩個頁面展示和操作同一個領域對象,卻有完全不同的展現和行為,這樣的場景並不少見。這引申出一個悲傷的結論:如果嚴格遵守FLUX數據流的同時按照domain切分store,很多 無領域狀態是無法管理的。
這並非無法解決,現在很多redux的示例或項目,store都是相對混雜的,領域模型和頁面狀態並存。
這種混雜當然不好看,而解決這種混雜,就是個人眼中社區的下一步趨勢:store由domain切分轉向頁面切分——畢竟我們想要復用的只是domain相關的邏輯(reducer)而已。如果總結一下的話,大家應該會有一種感覺,前端開發在很長一段時間內是服務於後端的,因此在框架、概念上都有頗深的後端印記,而FLUX、Redux們的出現,代表著開發者們真正從前端的角度在思考、組織和前進。
補充說明:以上說了很多前端MV*框架的問題,但所謂「問題」是在特定視角下的。對於前端而言,究竟是FLUX好還是MVVM好,個人心中並無定數,更無否定MVVM之意,相信兩者會並存很長一段時間。 之所以這麼說,是因為前面提及的前端MVC問題2——前端缺少唯一的完整應用狀態——這個假設是可以被質疑的:後端的唯一完整應用狀態只需要抽象成有限的領域模型然後建表即可,而前端要將頁面狀態考慮進去的話,實際上很難抽象。反而是MVVM的VM,在最頻繁變化的那一層有著極強表現力和靈活性。以下措辭,可能有錯誤, 希望指正。
我只看過Flux最初的定義 ,其實只是個擴展了的中介者模式(2. 中介者模式如果覺得麻煩,你可以看成一個EventBus),事實上我們平時即使不使用React這樣的框架,在大型工程里也會自然而然的形成這種模式。 因為分立組件間通過監聽者相互註冊事件很容易形成一個網狀結構,這種耦合性會帶來維護的困難。
好了言歸正傳, 這裡我談談為什麼React特別需要這樣的架構。
我曾經說過,也挨了不少噴: React其實也是個 臟檢查 , 只是Angular發生在數據層, 而React發生在承載結構的Virtual Dom 層而已 (可以看成一個是廣度的diff, 而一個是深度的樹diff).
臟檢查帶來的結果就是, 需要通知組件:「Hi, 我可能變了, 檢查一下我哦」. 比如React使用到setState來完成這個提醒動作, 但是有時候這個檢查是沒有必要的, shouldComponentUpdate 就出來了. 在確保這次狀態更改不會引起組件變化, 你就return false.
React在組件內部渲染的生命周期中,已經實現了很好的單向性.
內部行為(事件-非同步數據獲取) -&> 狀態更改 -&> render VD -&> diff VD 並映射DOM -&> IDLE等待下一個狀態更改. -&> ...
再回到React標榜它(或其他類似措辭的框架比如Vue)只是個View, 這應該是從它在產品代碼結構上的定位來說的,從組件本身來講, 其實內部 有Model 也有相關的Logic. 既然這樣,就會遇到不同組件依賴同一份數據的情況, 但是分立的組件的檢查過程是獨立的, 這樣可能會出現網狀的通知鏈, 這在大規模的軟體開發中, 會導致維護不利.
一個理想的流程是:
數據源broadcast「我變了」 -&> 通知到到所有感興趣的的組件 -&> 組件內部的渲染流程 -&> IDLE等下一個通知, 這樣作為View的React Component 和所處的流程就都是 單向的了, 再加入 ACTION (觸發更改的行為的抽象 ) 和 管理者Dispatcher, 就形成了所謂的Flux模式. 我覺得FB里提出Flux的大牛們, 可能沒有仔細看過設計模式 或是 MVC定義, 但是並不影響 Flux是一個非常適合構建大型React工程的事實. 解決問題了就是好方案, 我也不喜歡太咬文嚼字的純學院風.原文鏈接:我理解的 Flux 架構-博客-雲棲社區-阿里雲
Flux 中的幾個基本概念
這是 Flux 官方提供的一張說明圖:
圖中有四個名詞:
- View
- Store
- Action
- Dispatcher
下面逐個以我的角度做個講解:
首先 View 是視圖,是用戶看得見摸得著的地方,同時也是產生主要用戶交互的地方,這個概念在 MVC 和 MVVM 架構中都是有的,有些觀點認為雖然這幾種架構里都有 View,但是定義不太一致,有細微的差別,我自己覺得這種差異確實是存在的,但在一開始這並不妨礙我們理解 View 這個名詞。
然後是 Store,它對應我們傳統意義上的 Data,和 MVC、MVVM 里的 Model 有一定對應關係。你問我它們為啥不直接叫 Data 算了,那這就是文化人和小老百姓表達方式的差別。當然了我只是想盡量降低理解成本,嘗試用比較通俗的說法把問題說清楚。
然後是 Action,這看上去是一個新概念,實際上我還是能找到一些幫助大家理解的名詞,叫做 Event。就是一個結構化的信息,從一個地方傳遞到另一個地方,整個過程就是一個 Action/Event。
最後是 Dispatcher,多說一句,我覺得正是因為有了 Dispatcher 才讓前面三個名詞變得有新鮮感。也是理解 Flux 的關鍵。言歸正傳,Dispatcher 算是從 Action 觸發到導致 Store 改變的鎮流器。比一般架構設計里直接在「Event」邏輯中修改「Data」更「正規」。所以土得掉渣的 Event 變成了 Action,土得掉渣的 Data 變成了 Store,土得掉渣的 View 仍然是土得掉渣的 View。
為什麼多了 Dispatcher,這些 Store、View、Action 就變得神奇了呢?
因為「正規」
傳統 MVC 被 Flux 團隊吐槽最深的,表面上是 Controller 中心化不利於擴展,實際上是 Controller 需要處理大量複雜的 Event 導致。這裡的 Event 可能來自各個方向是 Flux 吐槽的第二個點,所以不同的數據被不同方向的不同類型的 Event 修改,數據和數據之間可能還有聯繫,難免就會亂。
所以和 Dispatcher 配合的 Store 只剩下了一個修改來源,和 Dispatcher 配合的 Action 只剩下了約定好的有限幾種操作。一下子最混亂的地方變得異常「正規」了。架構複雜度自然就得到了有效的控制。
另外還有一個蠻好理解的點是:Action 不僅僅把修改 Store 的方式約束了起來,同時還加大了 Store 操作的顆粒度,讓瑣碎的數據變更變得清晰有意義。
另外,這兩個地方抽象之後數據操作變得「無狀態」了,所以可以根據 Action 的歷史記錄確定 Store 的狀態。這個讓很多撤銷恢復管理等場景成為了可能。
綜上所述,在 Flux 架構中,數據修改的顆粒度變大,更有語義;上層數據操作的行為更抽象化,碎片化程度降低。
Flux 架構是 React 技術棧獨佔的嗎?
不是,只要在傳統架構的基礎上注重對數據操作和用戶/客戶端/伺服器行為的抽象定義,Flux 架構中提到的各種好處大家都享受得到。
我們就拿被 Flux 黑得最慘的那個「一大堆 V 和一大堆 M 只有一個 C」的例子好了,圖中每個 View 找到不一樣 Model 進行操作時,我們把這些操作抽象成 Action,然後通過中心化的邏輯找到相應的 Model 完成修改,其實就是 Flux 了。這裡抽象出來的 Action 一定要和圖中 Controller 能夠接受到 Action 一樣,沒有什麼特殊的地方。
基於這樣的理解,Redux 提出了另外的對 Flux 架構的理解:
- 首先 Store 是通過 Creator 創建出來的
- 每個 Store 都有自己的 state 用來記錄當前狀態
- 在創建 Store 的時候,通過 Reducer 把 state 和 action 的關係建立起來
- 後期通過在 Store 對象上 dispatch 不同的 action 達到對 state 的修改
本質上同樣是對數據操作和上層行為的抽象,另外從實現層面更加 functional。
Vuex 是基於 Vue.js 的架構設計,稍後再展開說我的看法。
Flux 架構有什麼不為人知的坑嗎?我們就像看人黑 Flux!
(咳咳咳~~~ 這個問題我得謹慎回答)
我覺得 Flux 架構沒有把一個事實告訴大家,就是它的 Store 是中心化的,Flux 用中心化的 Store 取代了它吐槽的中心化的 Controller。
我看了一些基於 Flux/Redux/Vuex 架構的實現,基本上多個 Store 之間完全解耦不建立任何聯繫是不可能的——除非它們完全從數據行為各方面都是解耦的——這種程序用什麼架構都無所謂的坦白講。
為什麼中心化的 Store 無人吐槽呢?因為中心化的數據複雜度絕對低於中心化的行為控制。你甚至沒有意識到它是中心化的,這其實從另外一個側面就證實了這一點。
所以我覺得透過 Flux 看架構的本質:這裡不算是坑或吐槽,我更想說的是,放下 Flux 這把鎚子,我們該怎麼看世界,怎麼看待自己每天在設計和架構的軟體。
- 中心化管理數據,避免數據孤立,一旦數據被孤立,就需要通過其它程序做串聯,導致複雜。這是避免各路行為亂改數據導致混亂的一個潛在條件,或者說這是一個結論。
- 把行為做個歸納,抽象度提高,不管是用戶操作導致的,還是從伺服器 pull 過來的,還是系統本身操作的。
- 把修改數據的操作做個歸納,顆粒度變大,大到純粹「無狀態」的極限。
- 另外一個沒有被過多談論的細節,就是從 Model 到 View 要簡單直接,這一點各路架構都是有共識的,就不多說了。
在這幾個方面,如果一個架構師能夠做到極致,去TM的各種架構縮寫,用哪個都一樣。
Vuex 怎麼樣?
我先說我覺得 Vue.js 怎麼樣,Vue.js 天生做了幾件事:
- components,即組件化,把視圖分解開
- 通過 computed options 簡化 data 到 template 的對應關係
- 通過 methods options 明確各路行為的抽象
- 通過雙向 computed options 增大了對 data 操作的顆粒度
- 部分 methods options 也可以用來完成純粹的 data 操作,增大對 data 操作的顆粒度
所以 Vue.js 本身已經提供了很多很好的架構實踐。但這在 Flux 看來還不夠純粹,它缺 2 點:
- 數據有 components 之間的樹形關聯,但是修改起來是分散的
- 相應的 computed、methods 也應該不是分散的,需要改造
所以 Vuex 需要做的事情很簡單:
- 中心化的 store,所有 components 都共用一份數據,即一份 state;更複雜的情況下,定義有限的幾種 getters,用在 computed options 中
- 定義有限的幾種 mutations (類比從 Dispatcher 到 Store 的約定),可以直接用在 methods options 中;更複雜情況下,定義有限的幾種 actions (類比從各路行為到 Dispatcher 的約定),用在 methods options 中,背後調用的是各種定義好的 mutations。
這樣在 Vue 的基礎上,再加上如虎添翼的 Vuex,開發者就可以享受到類似 Flux 的感覺了。
都快說完了都沒提「單向數據流」這個詞
是的,我覺得這是一個被用爛的詞,以至於很多人在求職面試的時候一被問到 Flux 就脫口而出「單向數據流」,幾乎當做 Flux 這個詞的中文翻譯在回答。就好像一說到 Scrum 就脫口而出「看板」一樣……
我覺得單向數據流的講法太過表面,不足夠體現出 Flux 的設想和用意。現在一提單向數據流,我腦中第一個浮現的畫面其實是這個:
都快說完了都沒提「時空穿梭 (time travel)」這個詞
這是數據操作顆粒度變大之後的名詞。我覺得它只是個名詞,為什麼這樣說?
所為「時空穿梭」,本質就是記錄下每一次數據修改,只要每次修改都是無狀態的,那麼我們理論上就可以通過修改記錄還原之前任意時刻的數據。
大家設想一下,其實我們每次對數據最小顆粒度的、不能再分解的、最直接的操作基本 (比如賦值、刪除、增減數據項目等) 都是無狀態的,其實我們如果寫個簡單的程序,把每次直接修改數據的操作記錄下來,同樣可以很精細的進行「時空穿梭」,但沒有人提這個詞,因為它顆粒度太細了,沒有語義,沒有人願意在這樣瑣碎的數據操作中提煉「時空」。因為數據操作的顆粒度變大了,所以變得直觀,有語義,易於理解,對我們的功能研發和調試有實際幫助,所以才有了「時空穿梭」這個概念。
Weex 什麼時候支持 Flux/Vuex?
這是我最後想說的,首先不管有沒有 Flux/Vuex,一個好的架構實踐已經足以滿足日常的研發需求,尤其是在手機上,界面、數據和行為都不會特別複雜。
其次,如果基於 Vue 2.0 來開發 Weex 頁面或應用的話,Vuex 是天生支持的,不需要額外做什麼。大家如果已經在瀏覽器中,不論是桌面還是手機上實踐過 Vuex,應該是感覺不到任何不一樣的。
最後,上周我簡單寫了個 Vuex 的復刻版,能夠在 Weex 的 JS Framework 上工作,這裡不想占太多篇幅介紹。坦白講我希望大家更多的精力在理解 Flux 和 Vue 上。其它問題都是順理成章的。
總結
這篇文章整理了我個人對 Flux 的理解和個人看法,首先解釋一下 Flux 核心的四個名詞:View, Store, Action, Dispatcher,然後提出 Dispatcher 在 Flux 架構中的關鍵位置,並解釋為什麼 Dispatcher 讓其他三者變得更好更「正規」,然後是一些我通過了解 Flux 認識到的背後倡導的架構設計的最佳實踐的提煉。
真的沒有代碼……
……好吧如果一定要看代碼可以看看這裡
- https://github.com/reactjs/redux
- https://github.com/vuejs/vuex
- https://github.com/Jinjiang/weex-x
一句話講明白:Flux就是手動將Action從數據流底層視圖中的事件手動綁定到數據頂層的數據流架構。補充對照:Reactive Programming是在數據源就綁定好直接交給底層視圖。參照第二個概念發現Redux的bindActionCreator就是手動模仿FRP的思路:在數據頂層通過綁定預先約定好將要發生的事件並監聽,當事件發生時改變數據自己—然後底層視圖就被動地跟著變化了。
Flux 算不上框架,只能說是當 View 已經是 React 之後,如何在不破壞 View 的無狀態的前提下,怎麼為其提供必要的數據的方法。這個方法涵蓋了 MVC 中 MC 的工作。和 FRP 沒有直接關係,事實上,當你用了 React 之後,再結合 FRP 就變得困難了。
說說自己的理解。
如果你清楚下面幾個的區別:- MVC
- MVP(Passive View)
- MVP(Supervising Controller)
- MVVM
那麼,Flux 說白了就是一個改良版的 MVP(Passive View),View 不能搞 Model 早就不是什麼新鮮的想法。
2006年的文章:Passive View
This pattern is yet another variation on model-view-controller and model-view-presenter. As with these the UI is split between a view that handles display and a controller that responds to user gestures. The significant change with Passive View is that the view is made completely passive and is no longer responsible for updating itself from the model. As a result all of the view logic is in the controller. As a result, there is no dependencies in either direction between the view and the model.
還有2010年的MSDN:Model-View-Presenter
在傳統的 MVP(Passive View) 模式中,一個 View 對應一個 Presenter,一個 Presenter 可能有依賴多個 Model。當用戶操作 View 的時候,會把事件的處理權交給 Presenter,Presenter 通過 Model 的提供的 API 操作 Model。Model 如果發生了改變,就會通過觀察者模式通知所有依賴它的 Presenter,然後 Presenter 通過介面更新 View。
因為 Model 可能很多,也被很多 Presenter 依賴,所以一個 Model 的變化可能來源於不同的 Presenter,可能觸發到不同的 Presenter 不同的行為。應用複雜了就會很混亂。
怎麼辦呢?好了,我們規定 Presenter 不能直接操作 Model ,而是在 Presenter 和 Model 中間加一個叫 Dispatcher 的東西,全部 Presenter 所有對任何 Model 操作必須都走 Dispatcher 才能去搞 Model。然後把 Model 的名字換成 Store,把 Presenter 的名字換成 Controller-View(或者 Redux 裡面的 Container Component),以前手動更新視圖的操作交給 React(其實任意的 data-bind 庫也可以)去做,最後給它起個響亮的名字 Flux。
鑒於 MVVM 是 MVP 變種,所以其實任意的 MVP(Passive View)和 MVVM 都可以加上這一層。但是,嚴格的 MVC 模式中 V 是依賴 M 進行更新的,所以在 MVC 中不能應用這一模式。
===
Redux 做得比 Flux 更好。它把原本放在了 Presenter 層的應用狀態以及所有的 Model 都合併到了 一個 Store 裡面去,用 Reducers 來描述 Store 裡面不同部分的根據 Action 不同的變化,把應用狀態的問題順手解決了。默默吐槽一下,像 Todo List 這種對於 Angular 來說等價於維護一個數組的小玩具,用 Redux 寫起來是這樣子的:
示例:Todo List
對於這麼個簡單的小玩具,例子中定義了六個事件和三種層次不同的組件。
當然 Flux 是有它的優點的,它解決了傳統 MVC 框架下 View 和 Model 雙向綁定導致的關係混亂:
說白了就是把 上圖的 Model 和 Controller 拼成了一個叫 Dispatcher 的東西,然後再在 View 和 Model 中間加了一層叫 Store 的東西來整理M和V的關係(其實我感覺就是一個 ViewModel),然後 View 收到的任何 Action 都不再直接作用於 ViewModel,而是回到 Dispatcher,重新走一遍流程。
但是!大多數的 情況下是不會遇到這種混亂的 MVC 關係的(如果你遇到了很可能是你錯誤地使用了MVC),即使遇到也可以嘗試換一個 Model 結構來解決。
Flux 帶來的好處就是,發生的任何事情都在 dispatcher 中提前定義好了(沒定義的話就 不會發生任何事情),所以任何事件都是可預測的。但是上面的例子中一個 Todo List 就搞出三個事件,所以 dispatcher 一定會存在臃腫的問題,當然你可以通過定義多個 dispatcher 來拆分邏輯,但依然沒有解決本質問題。
有時候在 Angular 下一個邏輯非常簡單的雙向綁定,用 Flux 的方法來寫真的是褪一層皮,倒不是在乎代碼量多,而是把一個本不用拆分的簡單邏輯硬生生拆分成了 Disptcher -&> Store -&> View 三層,帶來的維護成本也是很高的。
總之就是,Flux 的結構在極端情況下也能遊刃有餘,所以它號稱是「一種適合大型項目的架構」(拿它寫一個 Todo List 你一定是腦子有問題),它非常適合構建類似 Strikingly - 可能是移動時代最出色的免費建站工具 這樣複雜的編輯界面。但對於前端遇到的90%的需求,其實還是 Angular 效率更高。當一個事情全是讚揚的時候,應該就說明還是有些地方出問題了吧,要不我就回答下吧。
FaceBook 也是有一幫技術大牛,需要一些新的 OKR(KPI),各位還是要保持著一顆冷靜的心來看待這些新概念和新東西。
先說 React,React 本身並沒有特別多讓我激動的點,當然 React Native 還有點意思,可以再研究研究。
React 本身只是一個 View,之前也有很多能夠實現組件化封裝的庫,遠的很多名字我記不清楚了,近的有 Angular 的 directive、Polymer、甚至是直接寫 Web Component 等。Virtual Dom 的概念,其實就是編程中的 cache,以前寫模板引擎的時候也都會生成一個類似的東西(編譯後的 JS 函數),如果要生成 Dom ,有的也會放到 Fragment 中,不會直接在頁面上生成。當然 React 不是這樣實現的,但 Virtual Dom 也是類似的思路。感覺 React 最大的創新點應該是 JSX 編譯上面,而且能夠推動一幫人做了 iOS 和 Android 版本。這玩意用在 Web 上面本身其實沒有什麼真的特殊的優勢,但是如果結合 iOS 和 Android 確實是解決了開發效率的問題。
Flux 這個概念說的那麼高大上,還說了很多 MVC 的問題,但是。。。這些問題也都是隨著項目不斷複雜累積上去的。Flux 的 Store 給人感覺就是一個大大的狀態機(翻譯下,就是一個大 Object),然後管理著不同模塊的數據,居然通過 Dispatcher 一個大大的事件中心來處理和分發所有操作及數據。好吧,有很多人居然說這個簡潔,高手其實用什麼工具寫都是很簡潔的,問題是很多人使用單一事件中心就會很容易出現時序問題、事件爆炸。。。項目大了如何維護?一個事件一個事件的找對應,關鍵是事件一個很分散的東西,很可能多個位置會被一個事件所觸發。不過,也可能我沒有深入使用,不知道 Flux 是否增加了強約束,但這樣又太不靈活,模塊間無法通信。
目前來看,如果在純 Web 上面使用 React 並沒有在開發上面有什麼優勢,但是支持 IE8 和伺服器渲染首屏還是可以嘗試的,大項目如果要用要三思,因為沒有一個整體的方案。而且 React 去掉了雙向綁定之類的東西,個人開發體驗上覺得沒有 Angular 好用,成體系。
end
React 很好, 但是有一點問題是 model 變化了怎麼通知程序重繪 dom 呢?
笨人方法是每次都手動調用...
方法二是 watch object, 但不能嵌套 watch 所以代碼會很繁瑣, angular 是這麼做的
方法三是包裝 model 而不用裸 object, 每次 set attribute 都重新 render 一次: backbone 是這麼做的 immutable model 也可以做類似的工作, 因為任何一個小屬性的變化都必須替換根 model: Om 是這麼做的.
方法四也是每次更新都觸發重繪, 不過用 _.throttle 或者 requestAnimationFrame 去限制調用次數, 就不會做太多額外工作了.
方法五是用 Flux, 現在你忙著理解架構和找代碼, 就顧不得抱怨這個問題的複雜啦我們公司的前端架構就是參考了這個的,雖然思路單向,卻能非常清晰的實現業務,排查起來也是按這個方向來,不會出錯
這是本人之前使用和理解的,不算是很深入,說的不對勿噴。這是一種單向的數據流,是一種設計思想,並不是框架。在實際的使用中可以把Action 和 Dispatcher 根據需要相互融合。V在componentDidMount的時候監聽一個數據變化的事件。C就是dispatcher去分發操作,M更新完數據以後,觸發一個change,通知到V,然後V就相應的更新。當然V也是可以不監聽change。
用flux寫了兩個項目
像mvc 或者說就是啦
思路很清晰
可是最大的問題就是trigger之後,無法讓component除了渲染以外再做一些別的事件推薦閱讀:
※js中alert函數的實現原理是什麼?
※請問js非同步和同步載入在性能優化中有什麼區別?
※vs code中,寫在<script></script>里的js代碼沒提示么?
※請問如何學習nodejs並且達到能開發類似fis,spm的水平 有好的教程嗎?
※對於前端,有哪些好的chrome插件或應用可以使用?
TAG:前端開發 | JavaScript | 軟體架構 | React | Flux |