精讀《前端數據流哲學》

本系列分三部曲:《框架實現》 《框架使用》 與 《數據流哲學》,這三篇是我對數據流階段性的總結,正好補充以前過期的文章。javascript

本篇是收官之做 《前端數據流哲學》。css

1 引言

寫這篇文章時,頗有壓力,若有不妥之處,歡迎指正。html

同時,因爲這是一篇佛系文章,因此不會得出你應該用 某某 框架的結論,你應該看成消遣來閱讀。前端

2 精讀

首先數據流管理模式,比較熱門的分爲三種。vue

  • 函數式、不可變、模式化。典型實現:Redux - 簡直是正義的化身。
  • 響應式、依賴追蹤。典型實現:Mobx。
  • 響應式,和樓上區別是以流的形式實現。典型實現:Rxjs、xstream。

固然還有第四種模式,裸奔,其實有時候也挺健康的。java

數據流使用通用的準則是:反作用隔離、全局與局部狀態的合理劃分,以上三種數據流管理模式均可以實現,惟有是否強制的區別。react

2.1 從時間順序提及

一直在思考如何將這三個思惟串起來,後來想通了,按照時間順序串起來就很是天然。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 的順序解讀這三個框架。

2.2 redux 帶來了什麼

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 的項目,維護的時候須要頻繁使用全文搜索,以及至少在兩個文件間來回跳躍。

2.3 mobx 帶來了什麼

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 找到結合點,前端探索的腳步從未中止。

2.4 rxjs 帶來了什麼

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 串了起來,還發現了他們內在的關聯,這三個思想像一張網,複雜的交織在一塊兒。

2.5 能夠串起來些什麼了

咱們發現,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 取代。

再次理解 react + mobx 不如用 vue 這句話

首先這句話頗有道理,也頗有份量,不過筆者今天將從一個全新的角度思考。

通過前面的探討,能夠發現,如今前端開發過程分爲三個部分:反作用隔離 -> 數據流驅動 -> 視圖渲染。

先看視圖渲染,不管是 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 形式。

對框架封裝的抽象度越高,框架之間差別就越小,漸漸的,咱們會從框架名稱的討論中解放,演變成對框架 + 數據流哪一種組合更加合適的思考。

3 總結

最近梳理了一下 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 會在更大範圍被使用。

縱觀前端歷史,數據流框架從無到有,但在將來極有可能從有變到無,前端數據流框架消失了,但前端數據流思想永遠保留了下來,變得無處不在。

4 更多討論

討論地址是:精讀《前端數據流哲學》 · Issue #58 · dt-fe/weekly

若是你想參與討論,請點擊這裏,每週都有新的主題,每週五發布。

相關文章
相關標籤/搜索