Redux 的狀態管理理念很是優雅,隨之附帶的時間旅行調試支持也很是酷炫。但這個特性是不是傳說中的銀彈,又會給使用者帶來什麼額外的負擔呢?讓咱們從新思考一下吧。前端
在 2015 年的 React Europe 會議上,Dan Abramov 展現了經過 Redux DevTools 讓開發者在歷史狀態中自由穿梭,從而提高調試體驗的 Demo,這個工具的使用體驗很是驚豔,也取得了很是好的反響。在此以後,Vuex 與 MobX 等狀態管理庫也陸續在它們的調試工具中引入了對相似功能的支持。git
咱們能夠認爲,前端狀態管理領域中,狹義的『時間旅行』概念是在知足了下面這幾個前提後,開發時在歷史狀態中任意回溯的功能:github
須要特別注意的是,這個功能徹底是調試時使用的。不過,因爲這個能力給人的印象過於深入,它也成爲了許多人轉向 React + Redux 技術棧的主要理由之一:漂亮的概念模型加上漂亮的調試體驗,這套方案簡直就是神器啊!而正如 React 第一個在瀏覽器裏實現了聲明式渲染同樣,Redux 也第一個在瀏覽器裏實現了理想中的調試體驗,這些原創性的工做對前端領域的貢獻是很是大的。在下文中,咱們對 React + Redux 一些潛在問題的分析,也是創建在尊重社區工做的基礎上的。面試
在剛剛結束的 D2 上,筆者雖然沒有看到徹底顛覆性的新輪子,但對於很多開放性的問題得到了全新的答案。這其中的一個問題幫助筆者從新梳理了對前端的理解,並構成了本節最主要的論據。這個問題是:前端的複雜應用該如何分類?算法
傳統上,咱們會將功能做爲區分應用類別的維度。好比:管理後臺、活動 H五、聊天 IM、電商購物、視頻直播……咱們有很是多細分領域,每一個領域都有不一樣的業務痛點和側重點,這樣看來要想一通百通地『打通任督二脈』是很困難的。但有沒有更簡單的劃分方式呢?這裏,咱們有了一個更簡單的答案,即將複雜的前端應用簡單地分爲兩類:數據驅動和事件驅動。編程
這類應用的業務複雜度徹底來自於後臺無窮無盡的數據和複雜業務流程。好比,一個購物網站的瀏覽頁並無太多的輸入須要處理,但來自後端接口的商品數據能夠是千人千面的;再好比 12306 的訂票平臺,雖然它的前端界面顯得簡陋,但整個業務流程的複雜度可能不是一個普通用戶甚至開發者所能想象的。概況地說,這類最多讓用戶填幾個表單和驗證碼的應用,業務邏輯裏的坑有多深經常只有摸過的同窗才懂。這些應用均可以理解爲是數據驅動的。redux
相比之下,事件驅動的前端應用,其複雜度則來自於用戶的輸入事件。好比,一個富文本編輯器在編輯時就算徹底不對接後臺接口,光是處理用戶的粘貼、選中和鍵盤等事件,就能夠成爲傳說中的『天坑』;再好比一個 H5 版的《太鼓達人》遊戲只須要從後端拉取靜態的音樂資源,但用戶點擊的節奏只要差上幾十毫秒,界面的狀態和最後的結果均可能徹底不一樣。構建這類應用的時候,其難點主要來自於在大量不一樣類型的異步事件能夠任意地排列組合,使得可能的狀態空間極度膨脹而容易出錯——相信只要在頁面中同時維護過幾個定時器的同窗都能理解。咱們能夠把這樣的應用歸類爲事件驅動的。後端
時間旅行的概念,和上面說起的兩種應用分類有什麼關係呢?這牽扯到不少技術選型中決定使用 Redux 的動機:Redux 開發工具能支持時間旅行,因此咱們的應用在遇到相似須要回溯狀態的場景時,上 Redux 的風險更小。瀏覽器
這聽起來確實充分考慮了後期的拓展性,但它的問題在哪呢?一旦咱們從新考慮了對應用的分類維度,那麼對時間旅行的能力就會出現大相徑庭的需求:數據結構
從上面的討論中咱們能夠發現,只有對於事件驅動的前端應用,時間旅行的功能纔有意義(而且仍是極其重大的意義!)。而對於管理後臺等數據驅動的前端應用,時間旅行只是無關緊要的錦上添花罷了——這個業務場景下,把時間旅行做爲選擇 Redux 的重大理由,實在有些牽強。
相信不少同窗看到這裏會 argue 說,在管理後臺業務中使用 Redux 是有不少成功案例的,難道你認爲他們的架構師都是錯的嗎?而且,Redux 除了時間旅行外還有不少額外的好處,這些東西在決策時都比時間旅行重要得多呀!誠然,Redux 的流行程度已經證實它可以支撐『大規模』的前端應用,但框架的設計必定是伴隨着 trade-off 的。 在一個不須要時間旅行的業務場景下,Redux 中爲了實現時間旅行而引入的一些框架設計就會帶來額外的問題。 所以下面咱們要探討的問題就是:Redux 爲了率先實現時間旅行的特性,犧牲了哪些東西呢?
她那時候還太年輕,不知道全部命運贈送的禮物,早已在暗中標好了價格。
——《斷頭王后》
在剛剛發現 Redux 可以完全解決 React 中 props 層層傳遞的問題時,你們很是激動:哇你看這個無狀態的組件好優雅啊!哇你看只要所有狀態提到 store 裏,開發時咱們就能隨便絲般順滑地回退啦!很快,兩條最佳實踐出現了:
那麼,按照這兩條最佳實踐開發出的應用,會存在什麼問題呢?
在時間旅行的誘惑下,把所有狀態都交給 store 來管理,而後完全乾掉 setState
實在是太有誘惑裏了:不只能完美支持時間旅行,還能解決 React 裏一個貌似煩人的問題。然而把所有狀態交給 store 管理的時候,坑是少不了的,目前 Redux 在官方文檔裏對此的意見是 There is no "right" answer for this
,也就是說將所有狀態提到 store 中的實踐也能夠認爲是合理的。但真的是這樣嗎?
不知道有多少同窗在初學編程的時候,聽到過前輩這樣的告誡:少用全局變量。而 React 技術棧中看似高大上的全局狀態,只不過是拿 Context 粉飾一新的全局變量而已——你覺得穿了件 store
的馬甲人家就不認識你了嗎?全局變量該有的問題,全局狀態一個都躲不掉:
{a: {b: {c: {d: 1 }}}}
幾乎是必須藉助輔助工具的。對於一個富文本編輯器來講,若是想要表達『表格裏支持嵌套表格』的信息,Redux 對應的原生 JSON 數據結構也顯得很是單薄,基本必須上 Immutable——不過爲何我不直接使用 Immutable,跳過 Redux 這一層呢?筆者折騰過的 Slate.js 就是這麼作的。哦你說 Facebook 親生的 Draft.js 嗎?它用了 Immutable 沒錯,不過人家實現的是優雅的扁平數據結構,毫不支持表格這種僞需求的。到此爲止,對於 Redux 推崇的扁平全局 store,咱們已經有足夠多的理由來質疑了。雖然這麼設計 store 和時間旅行之間沒有直接的關係,但對『易於調試、易於推理、易於理解』的『優雅』的全局狀態,其誘惑頗有可能讓開發者踏進更大的陷阱裏。這是值得擔憂的。
固然了,Redux 確實解決了一個痛點問題,即深度嵌套的組件間狀態通訊的問題。但解決這個問題,並不表明着咱們就必須把狀態所有提到全局層面。這個問題的體現,能夠簡單理解爲: 在 A 組件裏實現的方法,觸發它的事件在 B 組件裏,而 C 組件又須要訂閱執行結果…… 這時候純 React 處理起來確實棘手,但只要將 store 放置在 A、B、C 三個組件中最頂級的一個裏——而不須要放置在全局——然後經過 Context 的定製,就足夠解決這個問題了。
另外一方面,對 Redux 廣泛的一個詬病在於它的 Boilerplate 代碼比較多,要發一個簡單的請求,都要 Action、Reducer、Middleware 走一波,思惟負擔比較大。這個細節其實和時間旅行的實現原理之間有着微妙的關係,簡單來講,能夠理解爲 Redux 爲了調試體驗,犧牲了開發體驗:
在 Dan Abramov 的演講裏,說起了 Webpack HMR 和 Redux DevTools 相結合所帶來的一個重要能力:一旦你更改了某個 Reducer 的代碼,那麼全部的 Action 都會從新求值,更新狀態。
咱們能夠把 HMR 的粒度理解爲函數級別的熱替換(此處筆者理解尚淺,有錯漏請務必指出),而 Redux 中實現狀態管理邏輯的最小粒度,剛好就是 Reducer 這樣的純函數。從而,對於 Dan 本人而言,在 Redux 的架構上實現這樣『只要發現某個函數被 patch 了,那麼就把全部 JSON 格式的 Action 從新跑一遍』的特性,就不須要什麼奇技淫巧的操做了——因而他在一週內就實現了 Redux DevTools,確實很是強!這時候的代價就是:使用 Redux 的開發者必須在開發階段使用這一套顯得繁重的機制,來使得 Dan 能輕鬆地改進調試體驗……技術上的取捨沒有絕對的對錯,對於開發和調試成本的權衡,這裏不作評論。
除了 Redux 對時間旅行的支持方式帶來的一些問題之外,另一種隱形的坑在於這種想法:『Redux DevTools 對時間旅行支持得很好,因此在個人應用裏整合這個功能應該也不難。』前文已經說起,在實現一個事件驅動型的前端應用時,時間旅行的功能確實特別重要。但實現這個特性的難度,恐怕不是拉進一個 Redux 就能簡單實現的。這裏以富文本編輯這個事件驅動型應用爲例,列舉幾個業務中遇到的具體例子:
這些場景裏,針對每一個案例的解決方案都和 Redux 的理念沒有太多關係。而對於一些複雜度更高的場景(如富文本編輯的實時協同),這時實現時間旅行的基礎就已經再也不是簡單的撤銷棧 + 全量狀態替換,而是已經涉及到 OT 等足夠寫很多論文的高級算法了。這樣看來,事件驅動型的應用裏,若是須要實現時間旅行類型的功能,阻礙有二:
所以這裏的問題總結而言也比較諷刺:在須要時間旅行特性的應用裏,Redux 除了引入它的一套約定外,幫不上什麼忙。再結合上文的討論,你能夠發現對於時間旅行而言,它在數據驅動的應用裏基本不須要實現,而在事件驅動的應用裏實現時,Redux 的幫助也頗有限……
這篇文章不是來推銷新輪子的,不過對於上文中的兩種應用場景,咱們都確實地發現有更合適的狀態管理方案選擇。MobX 和 RxJS 是筆者以前有偏好的兩個庫,在從新審視場景後,會發現它們剛好各有所長:
數據驅動的應用中,領域模型極可能很是細碎而繁多(好比對於每種不一樣的表單,均可以有本身的數據模型),並且對於每種領域模型,封裝出與之對應的增查改刪能力就基本足夠知足需求了。這時候,MobX 狀態管理的抽象顯得很是天然:
須要注意的是,MobX 在重繪時的性能優點是以訪問劫持後更大的內存佔用爲代價的。關於這個 trade-off,筆者在 D2 上剛好也向分享 Web 優化的 UC 內核開發者講師諮詢了內存佔用對前端性能的影響。根據 dalao 的回覆,這方面主要的案例仍然是來自於大量下載圖片等明顯的反模式,而狀態管理中數據模型的內存消耗則不是一個影響性能的瓶頸點。從這個角度來看,MobX 在設計上的權衡與取捨能夠認爲是值得的。
事件驅動的前端應用中,對異步邏輯的把握則顯得很是重要。這方面,redux-saga
一類的庫提供了一些處理異步反作用的方式,但若是你瞭解了 RxJS,會發現 Saga 看似強大的能力在 Rx 的事件流思惟模型面前,簡直就是玩具。
若是用數據驅動應用的思惟來理解 RxJS,你只會感受它的 API 十分沉重,侵入性很強。實際上,你須要在事件驅動的場景下來感覺這一套理念的強大。這裏的一個例子,是天天等電梯時電梯的調度方式:電梯的狀態直接由用戶按下樓層按鈕的事件流所決定,這時經過 RxJS 的響應式編程可以很合理地建模這個業務。做爲從例子出發學習 RxJS 的教程,筆者以前撰寫過一篇《響應式編程入門:實現電梯調度模擬器》的專欄,還有一個配套的 Demo 實現,歡迎有興趣的同窗閱讀。
毫無疑問,時間旅行是一個強大的調試特性。本文討論的是將時間旅行從調試工具向業務中落地時,可能涉及的一些問題:數據驅動的前端應用對它的需求不大;Redux 實現時間旅行的特性帶來了一些反模式;實現時間旅行時要處理的其它技術細節大大超出了 Redux 所能處理的範疇等。做爲替代,基於 OO 的狀態管理工具 MobX 和基於響應式編程的 RxJS 是筆者在不一樣場景下更青睞的。對於 GraphQL 等文中沒有涉及到的新輪子,但願有相關經驗的讀者 dalao 能不吝賜教。
本文看起來到處都在針對 Redux,雖然這裏確實存在一些利益相關(筆者始終不太喜歡它,對它的使用也不如 MobX、RxJS 甚至 Vuex 深),但文中的結論是以實際的場景做爲支撐的,絕對沒有 Redux API 好難學因此它確定很爛
這樣的想法。而 Redux 團隊的工做,也是很是值得尊敬的。若是文中有任何對 Redux 和時間旅行在理解上的誤差,但願讀者指出,我也很是願意根據討論去修正、優化本身的觀念。
最後的一點私貨,是筆者對前端『圈子』的一點理解:我的發現這個領域裏不少人對於平常使用的框架和工具備着一種盲目的崇拜情緒:不容許別人評論本身所用框架的問題;將框架的設計問題解釋成『你很差用是由於你水平不夠』的玄學問題;給同類工具直接貼上『很差』的標籤……或許這確實體現了某種對前端的『執着和熱愛』,但這也使得國內社區的討論氛圍相比國外,顯得很糟糕。筆者在面試時喜歡提的一個開放性問題是『你偏好的這個框架有哪些很差?』,這個問題不只有區分度(許多表現平庸的候選人經常爲了體現本身對框架的熟悉,直接回答『我以爲沒有什麼很差』……),而且反向的思考其實更有助於咱們去結合實際場景,理解框架設計的原理和取捨。
感謝堅持看到這裏的你,但願本文能對你有所幫助~