本系列分三部曲:《框架實現》 《框架使用》 與 《數據流哲學》,這三篇是我對數據流階段性的總結,正好補充以前過期的文章。javascript
本篇是收官之做 《前端數據流哲學》。css
寫這篇文章時,頗有壓力,若有不妥之處,歡迎指正。html
同時,因爲這是一篇佛系文章,因此不會得出你應該用 某某 框架的結論,你應該看成消遣來閱讀。前端
首先數據流管理模式,比較熱門的分爲三種。vue
固然還有第四種模式,裸奔,其實有時候也挺健康的。java
數據流使用通用的準則是:反作用隔離、全局與局部狀態的合理劃分,以上三種數據流管理模式均可以實現,惟有是否強制的區別。react
一直在思考如何將這三個思惟串起來,後來想通了,按照時間順序串起來就很是天然。jquery
暫時略過 Prototype、jquery 時代,爲何略過呢?由於當時前端還在野蠻人時代,生存問題都沒有解決,哪還有功夫思考什麼數據流,設計模式?前端也是那時候被以爲比後端水的。webpack
好在前端發展愈來愈健康,大坑小坑被不斷填上,加上硬件性能的提升,同時需求又愈來愈複雜,是時候想一想該如何組織代碼了。c++
最早映入眼簾的是 angular,搬來的 mvvm 思想真是爲前端開闢了新的世界,發現代碼還能夠這麼寫!雖然 angluar 用起來很重,但 mvvm 帶來的數據驅動思想已經愈來愈深刻人心,隨後 react 就忽然火起來了。
其實在 react 火起來以前,有一個框架一步到位,進入了 react + mobx 時代,對,就是 avalon。avalon 也很是火,可是一個框架要成功,必須天時、地利、人和,當時時機不對,你們處於 angular 疲憊期,大多投入了 react 的懷抱。
可能有些主觀,但我以爲 react 能火起來,主要由於你們認爲它就是輕量 angular + 繼承了數據驅動思想啊,很是符合時代背景,同時一大波概念被炒得火熱,狀態驅動、單向數據流等等,基本上用過 angular 的人都跟上了這波節奏。
雖然 react 內置了分形數據流管理體系,但老是強調本身只是 View 層,因而數據層加強的框架不斷涌現,從 flux、reflux、到 redux。不得不說,react 真的推進了數據流管理的獨立,讓咱們從新認識了數據流管理的重要性。
redux 概念太超前了,一步到位強制把反作用隔離掉了,但本身又沒有深刻解決帶來的代碼冗餘問題,讓咱們又愛又恨,因而一部分人把目光轉向了 mobx,這個響應式數據流框架,這個沒有強制分離反作用,因此寫起來很舒服的框架。
固然 mobx 若是僅僅是 mvvm 就不會火起來了,畢竟 angular 擺在那。主要是乘上了 react 這趟車,又有不少質疑 angular 髒檢測效率的聲音,mobx 也火了起來。固然,做爲前端的使命是優化人機交互,因此咱們都知道,用戶習慣是最難改變的,直到如今,redux 依然是絕對主流。
mobx 還在小範圍推廣時,另外一個更偏門的領域正剛處於萌芽期,就是 rxjs 爲表明的框架,和 mobx 公用一個 observable 名詞,你們 mobx 都沒搞清楚,更是不多人會去了解 rxjs。
當 mobx 逐漸展露頭角時,筆者作了一個相似的庫:dob。主要動機是 mobx 手感還不夠完美,對於新賦值變量須要用一些 extendObservable 等 api 修飾,正好發現瀏覽器對 proxy 支持已經成熟,所以筆者後來幾乎全部我的項目幾乎都用 dob 替代了 mobx。
這一時期三巨頭之一的 vue 火了起來,成功利用:若是 」react + mobx 很好用,那爲何不用 vue?「 的 flag 打動了我。
一直到如今,前端已經發展到可謂五花八門的地步,typescript 戰勝 flow 幾乎成爲了新的 js,出現了 ember、clojurescript 以後,各大語言也紛紛出了到 js 的編譯實現,陸陸續續的支持編譯到 webassembly,react 做者都棄坑 js 創造了新語言 reason。
以前寫過一篇初步認識 reason 的精讀。
能接下來這一套精神洗禮的前端們,已經養出心裏波瀾不驚的功夫,小衆已經不會成爲跨越溫馨區的門檻,再學個 rxjs 算啥呢?(開個玩笑,rxjs 社區不乏深耕多年的巨匠)因此最近 rxjs 又被炒的火熱。
因此,從時間順序來看,咱們能夠從 redux - mobx - rxjs 的順序解讀這三個框架。
redux 是強制使用全局 store 的框架,儘管無數人在嘗試將其作到局部化。
固然,一方面是因爲時代責任,那時須要一個全局狀態管理工具,彌補 react 局部數據流的不足。最重要的緣由,是 redux 擁有一套幾乎潔癖般完美的定位,就是要清晰、可回溯。
幾乎一切都是爲了這兩個詞準備的。第一步就要從分離反作用下手,由於反作用是阻礙代碼清晰、以及沒法回溯的第一道障礙,因此 action + reducer 概念閃亮登場,完美解決了反作用問題。多是參考了 koa 中間件的設計思路,redux middleware 將 action 對接到 reducer 的黑盒的控制權暴露給了開發者。
由 redux middleware 源碼閱讀引起的函數式熱,可能又拉近了開發者對 rxjs 的好感。同時高階函數概念也在中間件源碼中體現,幾乎是爲 react 高階組件作鋪墊。
社區出現了不少方案對 redux 異步作支持,從 redux-thunk 到 redux-saga,redux 帶來的異步隔離思想也逐漸深刻人心。同時基於此的一套高階封裝框架也層出不窮,建議用一個就好,好比 dva。
第二步就是解決阻礙回溯的「對象引用」機制,將 immutable 這套龐大思想搬到了前端。這下全部狀態都不會被修改,基於此的 redux-dev-tools 「時光機」 功能讓人印象深入。
Immutable 具體實現能夠參考筆者以前寫的一篇精讀:精讀 Immutable 結構共享。
固然,因爲很像事件機制的 dispatch
致使了 redux 對 ts 支持比較繁瑣,因此對 redux 的項目,維護的時候須要頻繁使用全文搜索,以及至少在兩個文件間來回跳躍。
mobx 是一個很是靈活的 TFRP 框架,是 FRP 的一個分支,將 FRP 作到了透明化,也能夠說是自動化。
從函數式(FP),到 FRP,再到 TFRP,之間只是拓展關係,並不意味着單詞越長越好。
以前說過了,因爲你們對 redux 的疲勞,讓 mobx 得以迅速壯大,不過如今要從另外一個角度分析。
mobx 帶來的概念從某種角度看,與 rxjs 很像,好比,都說本身的 observable 有多神奇。那麼 observable 究竟是啥呢?
能夠把 observable 理解爲信號源,每當信號變化時,函數流會自動執行,並輸出結果,對前端而言,最終會使視圖刷新。這就是數據驅動視圖。然而 mobx 是 TFRP 框架,每當變量變化時,都會自動觸發數據源的 dispatch,並且各視圖也是自動訂閱各數據源的,咱們稱爲依賴追蹤,或者叫自動依賴綁定。
筆者到如今仍是認爲,TFRP 是最高效的開發方式,自動訂閱 + 自動發佈,沒什麼比這個更高效了。
可是這種模式有一個隱患,它引起了反作用對純函數的污染,就像 redux 把 action 與 reducer 合起來了同樣。同時,對 props 的直接修改,也會致使與 react 對 props 的不可變定義衝突。所以 mobx 後來給出了 action 解決方案,解決了與 react props 的衝突,可是沒有解決反作用未強制分離的問題。
筆者認爲,反作用與 mutable 是兩件事,關於 mutable 與反作用的關係,後文會有說明。也就是 mobx 沒有解決反作用問題,不表明 TFRP 沒法分離反作用,並且 mutable 也不必定與 可回溯 衝突,好比 mobx-state-tree,就經過 mutable 的方式,完成了與 redux 的對接。
前端對數據流的探索還在繼續,mobx 先提供了一套獨有機制,後又與 redux 找到結合點,前端探索的腳步從未中止。
rxjs 是 FRP 的另外一個分支,是基於 Event Stream 的,因此從對 view 的輔助做用來講,相比 mobx,顯得不是那麼智能,可是對數據源的定義,和 TFRP 有着本質的區別,似的 rxjs 這類框架幾乎能夠將任何事件轉成數據源。
同時,rxjs 其對數據流處理能力很是強大,當咱們把前端的一切都轉爲數據源後,剩下的一切都由無所不能的 rxjs 作數據轉換,你會發現,反作用已經在數據源轉換這一層徹底隔離了,接下來會進入一個美妙的純函數世界,最後輸出到 dom driver 渲染,若是再加上虛擬 dom 的點綴,那豈不是。。豈不就是 cyclejs 嗎?
多提一句,rxjs 對數據流純函數的抽象能力很是強大,所以前端主要工做在於抽一個工具,將諸如事件、請求、推送等等反作用都轉化爲數據源。cyclejs 就是這樣一個框架:提供了一套上述的工具庫,與 dom 對接增長了虛擬 dom 能力。
rxjs 給前端數據流管理方案帶來了全新的視角,它的概念由 mobx 引起,但解題思路卻與 redux 類似。
rxjs 帶來了兩種新的開發方式,第一種是相似 cyclejs,將一切前端反作用轉化爲數據源,直接對接到 dom。另外一種是相似 redux-observable,將 rxjs 數據流處理能力融合到已有數據流框架中,
redux-observable 將 action 與 reducer 改造爲 stream 模式,對 action 中反作用行爲,好比發請求,也提供了封裝好的函數轉化爲數據源,所以,將 redux middleware 中的反作用,轉移到了數據源轉換作成中,讓 action 保持純函數,同時加強了本來就是純函數的 reducer 的數據處理能力,很是棒。
若是說 redux-saga 解決了異步,那麼 redux-observable 就是解決了反作用,同時贈送了 rxjs 數據處理能力。
回頭看一下 mobx,發現 rxjs 與 mobx 都有對 redux 的加強方案,前端數據流的發展就是在不斷交融。
咱們不但在時間線上,將 redux、mobx、rxjs 串了起來,還發現了他們內在的關聯,這三個思想像一張網,複雜的交織在一塊兒。
咱們發現,redux 和 rxjs 徹底隔離了反作用,是由於他們有一個共性,那就是對前端反作用的抽象。
redux 經過在 action 作反作用,將反作用隔離在 reducer 以外,使 reducer 成爲了純函數。
rxjs 將反作用先轉化爲數據源,將反作用隔離在管道流處理以外。
惟獨 mobx,缺乏了對反作用抽象這一層,因此致使了代碼寫的比 redux 和 rxjs 更爽,但反作用與純函數混雜在一塊兒,所以與函數式無緣。
有人會說,mobx 直接 mutable 改變對象也是致使反作用的緣由,筆者認爲是,也不是,看以下代碼:
obj.a = 1 複製代碼
這段代碼在 js 中鐵定是 mutable 的?不必定,一樣在 c++ 這些能夠重載運算符的語言中也不必定了,setter
語法不必定會修改原有對象,好比能夠經過 Object.defineProperty
來重寫 obj
對象的 setter
事件。
由此咱們能夠開一個腦洞,經過運算符重載,讓 mutable 方式獲得 immutable 的結果。在筆者博客 Redux 使用可變數據結構 有說明原理和用法,並且 mobx 做者 mweststrate 是這麼反駁那些吐槽 mobx 缺乏 redux 歷史回溯能力的聲音的:
autorun(() => { snapshots.push(Object.assign({}, obj)) }) 複製代碼
思路很簡單,在對象有改動時,保存一張快照,雖然性能可能有問題。這種簡單的想法開了個好頭,其實只要在框架層稍做改造,即可以實現 mutable 到 immutable 的轉換。
好比 mobx 做者的新做:immer 經過 proxy 元編程能力,將 setter
重寫爲 Object.assign()
實現 mutable 到 immutable 的轉換。
筆者的 dob-redux 也經過 proxy,調用 Immutablejs.set()
實現 mutable 到 immutable 的轉換。
真的是太看場景了。首先,業務場景的組件適合綁定全局數據流,業務無關的通用組件不適合綁定全局數據流。同時,對於複雜的通用組件,爲了更好的內部通訊,能夠綁定支持分形的數據流。
然而,若是數據流指的是 rxjs 對數據處理的過程,那麼任何須要數據複雜處理的場合,都適合使用 rxjs 進行數據計算。同時,若是數據流指的是對反作用的歸類,那任何反作用均可以利用 rxjs 轉成一個數據源歸一化。固然也能夠把反作用封裝成事件,或者 promise。
對於反作用歸一化,筆者認爲更適合使用 rxjs 來作,首先事件機制與 rxjs 很像,另外 promise 只能返回一次,並且以後 resolve
reject
兩種狀態,而 Observable 能夠返回屢次,並且沒有內置的狀態,因此能夠更加靈活的表示狀態。
因此對於各種業務場景,能夠先從人力、項目重要程度、後續維護成本等外部條件考慮,再根據具體組件在項目中使用場景,好比是否與業務綁定來肯定是否使用,以及怎麼使用數據流。
可能在不遠的將來,佈局和樣式工做會被 AI 取代,可是數據驅動下數據流選型應該比較難以被 AI 取代。
首先這句話頗有道理,也頗有份量,不過筆者今天將從一個全新的角度思考。
通過前面的探討,能夠發現,如今前端開發過程分爲三個部分:反作用隔離 -> 數據流驅動 -> 視圖渲染。
先看視圖渲染,不管是 jsx、或 template,都是相同的,能夠互相轉化的。
再看反作用隔離,通常來講框架也不解決這個問題,因此無論是 react/ag/vue + redux/mobx/rxjs 任何一種組合,最終你都不是靠前面的框架解決的,而是利用後面的 redux/mobx/rxjs 來解決。
最後看數據流驅動,不一樣框架內置的方式不一樣。react 內置的是類 redux 的方式,vue/angular 內置的是類 mobx 的方式,cyclejs 內置了 rxjs。
這麼來看,react + redux 是最天然的,react + mobx 就像 vue + redux 同樣,看上去不是很天然。也就是 react + mobx 彆扭的地方僅在於數據流驅動方式不一樣。對於視圖渲染、反作用隔離,這兩個因素不受任何組合的影響。
就數據流驅動問題來看,咱們能夠站在更高層面思考,好比將 react/vue/angular 的語法視爲三種 DSL 規範,那其實能夠用一種通用的 DSL 將其描述,並轉換對應的 DSL 對接不一樣框架(阿里內部已經有這種實現了)。而這個 DSL 對框架內置數據流處理過程也能夠屏蔽,舉個例子:
<button onClick={() => {
setState(() => {
data: {
name: 'nick'
}
})
}}>
{data.name}
</button>
複製代碼
若是咱們將上面的通用 jsx 代碼轉換爲通用 DSL 時,會使用通用的方式描述結構以及方法,而轉化爲具體 react/vue/angluar 代碼時,就會轉化爲對應內置數據流方案的實現。
因此其實內置數據流是什麼風格,在有了上層抽象後,是能夠忽略的,咱們甚至能夠利用 proxy,將 mutable 的代碼轉換到 react 時,改爲 immutable 模式,轉到 vue 時,保持 mutable 形式。
對框架封裝的抽象度越高,框架之間差別就越小,漸漸的,咱們會從框架名稱的討論中解放,演變成對框架 + 數據流哪一種組合更加合適的思考。
最近梳理了一下 gaea-editor - 筆者作的一個 web designer,從新思考了其中插件機制,拿出來說一講。
首先大致說明一下,這個編輯器使用 dob 做爲數據流,經過 react context 共享數據,寫法和 mobx 很像,不過這不是重點,重點是插件拓展機制也深度使用了數據流。
什麼是插件拓展機制?好比像 VScode 這些編輯器,都擁有強大的拓展能力,開發者想要添加一個功能,能夠不用學習其深奧的框架內容,而是讀一下簡單明瞭的插件文檔,使用插件完成想要功能的開發。解耦的很美好,不太重點是插件的能力是否強大,插件能夠觸及內核哪些功能、拿到哪些信息、擁有哪些能力?
筆者的想法比較激進,爲了讓插件擁有最大能力,這個 web designer 全部內核代碼都是用插件寫的,除了調用插件的部分。因此插件能夠隨意訪問和修改內核中任何數據,包括 UI。
讓 UI 擁有通用能力比較容易,gaea-editor 使用了插槽方式渲染 UI,也就是任何插件只要提供一個名字,就能嵌入到申明瞭對應名字的 UI 插槽中,而插件本身也能夠申明任意數量的插槽,內核中也有幾個內置的插槽。這樣插件的 UI 能力極強,任何 UI 均可以被新的插件替代掉,只要申明相同的名字便可。
剩下一半就是數據能力,筆者使用了依賴注入,將全部內核、插件的 store、action 全量注入到每個插件中:
@Connect class CustomPlugin extends React.PureComponent { render() { // this.props.Actions, this.props.Stores } } 複製代碼
同時,每一個插件能夠申明本身的 store,程序初始化時會合並全部插件的 store 到內存中。所以插件幾乎能夠作任何事,重寫一套內核也沒有問題,那麼作作拓展更是輕鬆。
其實這有點像 webpack 等插件的機制:
export default (context) => {} 複製代碼
每次申明插件,均可以從函數中拿到傳來的數據,那麼經過數據流的 Connect
能力,將數據注入到組件,也是一種強大的插件開發方式。
經過上面插件機制的例子會發現,數據流不只定義了數據處理方式、反作用隔離,同時依賴注入也在數據流功能列表之中,前端數據流是個很寬泛的概念,功能不少。
redux、mobx、rxjs 都擁有獨特的數據處理、反作用隔離方式,同時對應的框架 redux-react、mobx-react、cyclejs 都補充了各類方式的依賴注入,完成了與前端框架的銜接。正是應爲他們紛紛將內核能力抽象了出來,才讓 redux+rxjs mobx+rxjs 這些組合成爲了可能。
將來甚至會誕生一種徹底無數據管理能力的框架,只作純 view 層,內核原生對接 redux、mobx、rxjs 也不是沒有可能,由於框架自帶的數據流與這些數據流框架比起來,太弱了。
react stateless-component 就是一種嘗試,不過如今這種純 view 層組件配合數據流框架的方式還比較小衆。
純 view 層不表明沒有數據流管理功能,好比 props 的透傳,更新機制,均可以是內置的。
不過筆者認爲,將來的框架可能會朝着 view 與數據流徹底隔離的方式演化,這樣不但根本上解決了框架 + 數據流選擇之爭,還可讓框架更專一於解決 view 層的問題。
HTML5 有兩個有意思的標籤:details
, summary
。經過組合,能夠達到 details
默認隱藏,點擊 summary
能夠 toggle 控制 details
下內容的效果:
<details> <summary>標題</summary> <p>內容</p> </details> 複製代碼
更是能夠經過 css 覆蓋,徹底實現 collapse 組件的效果。
固然就 collapse 組件來講,由於其內部維持了狀態,因此控制摺疊面板的 打開/關閉 狀態,而 HTML5 的 details
也經過瀏覽器自身內部狀態,對開發者只暴露 css。
在將來,瀏覽器甚至可能提供更多的原生上層組件,而組件內部狀態愈來愈不須要開發者關心,甚至,不須要開發者再引用任何一個第三方通用組件,HTML 提供足夠多的基礎組件,開發者只須要引用 css 就能實現組件庫更換,彷佛回到了 bootstrap 時代。
有人會說,具備業務含義的再上層組件怎麼提供?別忘了 HTML components,這個規範配合瀏覽器實現了大量原生組件後,可能變得異常光彩奪目,DSL 不再須要了,HTML 自己就是一套通用的 DSL,框架更不須要了,瀏覽器內置了一套框架。
插一句題外話,全部組件都經過 html components 開發,就真正意義上實現了抹平框架,將來不須要前端框架,不須要 react 到 vue 的相互轉化,組件加載速度提升一個檔次,動態組件 load 可能只須要動態加載 css,也不用擔憂不一樣環境/框架下開發的組件沒法共存。前端發展老是在進兩步退一步,不要造成思惟定式,每隔一段時間,須要從新審視下舊的技術。
話題拉回來,從瀏覽器實現的 details
標籤來看,內部必定有狀態機制,假如這套狀態機制能夠提供給開發者,那數據流的 數據處理、反作用隔離、依賴注入 可能都是瀏覽器幫咱們作了,redux 和 mobx 會馬上失去優點,將來潛力最大的多是擁有強大純函數數據流處理能力的 rxjs。
固然在 2018 年,redux 和 mobx 依然會保持強大的活力,就算在將來瀏覽器內置的數據流機制,rxjs 可能也不適合大規模團隊合做,尤爲在如今有許多非前端崗位兼職前端的狀況下。
就像如今 facebook、google 的模式同樣,在將來的更多年內,先後端,甚至 dba 與算法崗位職能融合,每一個人都是全棧時,可能 rxjs 會在更大範圍被使用。
縱觀前端歷史,數據流框架從無到有,但在將來極有可能從有變到無,前端數據流框架消失了,但前端數據流思想永遠保留了下來,變得無處不在。
若是你想參與討論,請點擊這裏,每週都有新的主題,每週五發布。