[譯]Redux有多棒?

玄學醬發表於2017-10-16
本文講的是[譯] Redux 有多棒?,


Redux 有多棒?

1*BpaqVMW2RjQAg9cFHcX1pw.png

Redux 能夠優雅地處理複雜且難以被 React 元件描述的狀態互動。它本質上是一個訊息傳遞系統,就像在物件導向程式設計中看到的那樣,只是 Redux 是通過一個庫而不是在語言本身中來實現的。就像在 OOP 中那樣,Redux 將控制的責任從呼叫方轉移到了接收方 – 介面並不直接操作狀態值,而是釋出一條操作訊息來讓狀態解析。

一個 Redux store 是一個物件, reducers 是方法的處理程式,而 actions 是操作訊息。store.dispatch({ type: "foo", payload: "bar" }) 相當於 Ruby 中的 store.send(:foo, "bar")。中介軟體的使用方式類似於面向切面程式設計 (AOP, Aspect-Oriented Programming) (例如:Rails 中的 before_action)。 而 React-Redux 的 connect 則是依賴注入。

為什麼它值得稱讚?

  • 上文中控制許可權的轉移保證了當狀態轉換的實現變化時, UI 並不需要更新。新增複雜的功能,例如記錄日誌、撤銷操作,甚至是時光穿越除錯 (time travel debugging),將變得非常簡單。整合測試只需要確認派發了正確的 actions 即可,剩下的測試都可以通過單元測試來完成。
  • React 的元件狀態對於那些在 app 中觸及多個部分的狀態而言非常笨重,例如使用者資訊和訊息通知。Redux 提供了一個獨立於 UI 的狀態樹來處理這些交叉問題。此外,讓你的狀態存活於 UI 之外使實現資料可持久化之類的功能變得更簡單 – 你只需要在一個單獨的地方處理 localStorage 和 URL 即可。
  • Redux 的 reducer 提供了難以想象的靈活方式來處理 actions – 組合,多次派發,甚至 method_missing 式解析

這些都是不常見的情況。在常見情況下呢?

好吧,這就是問題所在。

  • 一個 action 可以被解釋為一個複雜的狀態轉換,但是它們中的絕大對數只是用來設定一個單獨的值。Redux 應用傾向於結束這一大堆只用於設定一個值的 action,這裡有個用於區分在 Java 中手動寫 setter 函式的標誌。
  • 可以在你 app 的任意一個地方使用狀態樹的任一部分,但是對於大多數狀態來說,它們一對一的對應了某個 UI 中的一部分。將這種狀態放在 Redux 中,而不是放在元件裡,這只是間接而非抽象
  • 一個 reducer 函式可以做各種奇怪的超程式設計,但是在絕大多數情況下它只是基於某個 action 型別的單一派發。這在 Elm 和 Erlang 這種語言中是很好實現的,因為在這些語言中,模式匹配是簡潔而高效的,但是在 JavaScript 中使用 switch 語句來實現就顯得格外笨拙。

但是更可怕的事是,當你花費了所有的時間在常見情況下編寫程式碼模板時,你會忘記,在某些特殊情況下會有更好的解決方案存在。你遇到了一個複雜的狀態轉換問題,然後呼叫了很多用於設定狀態值的 action 來解決了它。你在 reducer 中重複定義了很多狀態,而不是在 app 中分發同一個子狀態。你在很多 reducer 中複製貼上了各種 switch case 而不是把其中的某些方法抽象成共有的方法。

這很容易把這種錯誤僅僅當成 “操作員誤差” – 是他們沒有檢視操作手冊,就像可憐的工匠責怪他們手上的工具一樣 – 但是這種問題出現的頻率應當引起一些關注。如果大多數的人都錯誤的使用一款工具,那我們又該如何評價它呢?

所以我們應該避免在常見情況下使用 Redux,而把它留給特殊情況嗎?

這是 Redux 開發團隊給你的建議,也是我給我的開發團隊成員的建議:除非使用 setState 難以解決問題,不然儘量避免使用 Redux。但是我不能讓我自己也遵從我自己的規定,因為總是有某些原因讓你想要使用 Redux。 可能你有一系列的 set_$foo 訊息,而且設定這些值會更新 URL,或者重設某些瞬態值。可能你有一些明確和 UI 一對一的狀態值,但是你希望紀錄或者可以撤銷它們。

事實是,我不知道如何寫,更不要說指導寫“好的 Redux”。我曾經參與的每個 app 都充斥著 Redux 的反模式,因為我想不到更好的解決方案或者我無法說服我的隊友來改變它。如果一個 Redux “專家” 寫出來的程式碼也如此平庸,那我們還能指望一個新手怎麼做呢?無論如何,我只是希望能夠平衡一下現在大行其道的 “Redux 完成所有事” 解決方案,希望每個人都能在他們適用的情況下理解 Redux。

所以我們在這種情況下該怎麼做呢?

所幸的是,Redux 足夠靈活,我們可以使用第三方庫整合到 Redux 裡來解決常見情況 – 例如 Jumpstate。更清晰地說,我不認為 Redux 專注於處理底層事務是一種錯誤的行為。但是將這些基礎的功能外包給第三方來完成會造成額外的認知和開發負擔 – 每個使用者都需要從這些部分裡構建自己的框架。

有些人執著於此

而我正是其中之一。但並不是所有人都是。個人而言,我愛 Redux,儘可能地使用它,但是我仍舊喜愛嘗試新的 Webpack 設定。但是我並不代表絕大多數人群。我被實現靈活解決方案的心驅使著,在 Redux 的頂層寫了很多我自己的抽象方法。但是看著那些一群六個月前就離職的、從來沒留下開發記錄的開發工程師所寫的抽象程式,誰又能有動力呢?

其實很可能你根本不會遇到那些 Redux 特別擅長處理的難題,尤其如果你是一個團隊裡的新人,這些問題基本上會交給更資深的工程師處理。你在 Redux 上累積的經驗就是 “用著每個人都在用的垃圾庫,把所寫的程式碼都重複寫上好幾次”。 Redux 簡單到你可以不深入理解也能機械地使用它,但是那是一種很無聊也沒什麼提高的體驗。

這讓我回想起了我之前提出的一個問題:如果大多數的人都在錯誤的使用一款工具,那我們又該如何評價它呢?一個好的工具不僅僅應該有用且耐用 – 它應該讓使用者有個好的使用體驗。能舒服使用它的場景就是正確的場景。一個工具的設計不僅僅是為了它要完成的任務,同樣也要考慮到它的使用者。一個好的工具可以反映出工具製作者對於使用者的同情心。

那我們的同情心又在哪呢?為什麼我們的反應總是 “你錯誤地使用了它” 而不是 “我們可以把它設計地更容易去使用” 呢?

這裡有個函數語言程式設計界的相關現象,我喜歡叫它 Monad 指南的詛咒:解釋它們是怎麼工作的是非常簡單的,但是解釋清楚它們這麼做是有意義的就出乎意料地困難了。

在這篇文章中你真的要讀到一段 monad 指南?

Moand 是一個在 Haskell 常見的開發模式,在計算機中的很多地方都被廣泛使用 – 列表,錯誤處理,狀態,時間,輸入輸出。這裡有個語法糖,你可以以 do 表示式的形式像輸入指令程式碼一樣來輸入一系列的 monad 操作,就好像 javascript 中的 generator 可以讓非同步函式看起來像同步一樣。

第一個問題是,用 monad 用來做什麼來描述 monad 是不準確的。Haskell 曾引入 Monad 以解決副作用和順序計算,但是事實上 monad 作為一個抽象概念並不能解決副作用和順序化,它們是一系列規則,規定了一組函式如何互動,並沒有什麼固定的含義。關聯性的概念適用於算術集合操作、列表合併和 null 傳播,但是它完全獨立於這些操作。

第二個問題是在一些小問題上,用 monad 來解決問題更繁瑣了 – 至少看起來更復雜了 – 相比於指令式操作而言。給一個可選型別指定它的 Maybe Type 明顯比驗證一個模糊的 null 型別更安全,但是這又會讓程式碼變得更難看。使用 Either 型別來進行錯誤處理通常比那些隨處可能 throw 錯誤的程式碼更容易理解,但是 throw 操作的確比手動傳值更簡潔。而副作用 – 狀態,IO 等 – 在指令式語言中更是微不足道的。函數語言程式設計愛好者們(包括我)會說副作用在函式式語言中太簡單了,但是讓別人相信任何一種語言很簡單本身就是一件很難的事。

而 monad 真正的價值只能在巨集觀尺度體現出來 – 並不是這些用例都遵循著 monad 規則,但是這些用例都遵循著同樣的規則。能夠作用於一個用例的操作就可以作用於每個用例:把一對列表壓縮成一個儲存著對值的列表就和把一對 promise 函式融合成一個處理兩個結果的 promise 是“一樣的”。

所以呢?

現在 Redux 有同樣的問題 – 它很難學習並不是因為它很難反而是因為它太簡單。理解並不是認知的障礙,而要相信它的核心設計理念,我們才能通過歸納來延伸其它的知識。

這種思想是很難共享的,因為核心思想是無趣的真理(避免副作用)或者做一些無意義的抽象((prevState, action) => nextState)。任何單獨的例子都不會對這種理解有任何幫助,因為這些例子只是展示了 Redux 的細節但並不能展現它的核心思想。

一旦我們開始接受別人的思想,我們中的很多人就會立刻忘掉自己之前的一些想法。我們忘記了我們的理解只能從我們自己一次又一次的失敗和誤解中獲得。

所以你的建議是?

我覺得我們應該承認我們遇到了這個問題。Redux 是一種簡單卻不容易的語言。這是一種可以理解的設計選擇,但是仍舊是一種權衡。對於一門犧牲了某些簡單性來讓它更便於使用的語言,還是有很多人都會從中獲益的。但是,很多大型社群甚至不覺得這是一種已經做出的權衡。

我認為對比 React 和 Redux 是一件很有意思的事,因為廣泛來說 React 是更復雜的,它有著明顯更多 API 介面,同時它也在某種意義上更容易使用和理解。而 React 唯一必須的 API 介面是 React.createElementReactDOM.render – 狀態,元件生命週期,甚至 DOM 事件可以在別的地方處理。React 中的這些特性讓它變得更復雜,但是也讓它變得更出色

“原子化狀態”是個抽象概念,在你理解它之後可以指導你的開發,但是不管你理不理解這個概念,你都可以在 React 元件中呼叫 setState,來實現原子化狀態管理。這並不是一個完美的解決方案 – 徹底替換狀態或者強制更新有著比它更高的效率,而且它是一個非同步呼叫的方法還會產生一些 bug – 但是 React 將 setState 作為一個呼叫的方法而不是一個專業術語是一個很好的做法。

Redux 的開發組和社群都強烈反對增加 Redux 的 API 數量,但是現在將一堆小型開發庫融合在一起的做法對於專家而言是乏味的,而對於新手而言是費解的。如果 Redux 不能內建一些小功能來對常見情況做一些支援,那麼我們需要一個“更好”的框架在常見情況下來取代它。Jumpsuit 可以作為一個不錯的開始 – 它將“action”和“state”的概念轉化為了可呼叫的方法,同時保留了它們多對多的特性 – 但是事實上,這個庫其實並不關心這個優化本身。

諷刺的是:Redux 存在的意義 是“開發者體驗”:Dan 建立了 Redux 因為他希望理解和重建 Elm 的時光穿越除錯。但是隨著它開發了它自己的特性 – 進入了 React 生態系統的 OOP 執行環境 – 它犧牲了一些開發者的體驗以換取可配置性。這讓 Redux 得以蓬勃發展,但是這是個人性化開發框架明顯的缺失。我們,Redux 社群,準備好了嗎?


感謝 Matthew McVickar, a pile of moss, Eric Wood, Matt DuLeone, 和 Patrick Thomson review 本文。

備註:

[1] 為什麼要在 React / JS 和 OOP 之間做明顯的區分?JavaScript 是物件導向的,但是不是基於類(class-based)的。

OOP 類似於函數語言程式設計,是一種方法,不是某個語言特性。有些語言對於 OOP 支援地特別好,或者有一些專門為 OOP 定製的標準庫,但是如果你對它的瞭解夠深,你可以用任何語言寫出物件導向風格的程式碼。

JavaScript 有一種資料型別 Object,同時 JS 中大多數資料型別可以以 Object 的形式來處理和解析,從這種角度來說你可以對任何資料型別呼叫某些同樣的方法,除了 nullundefined。但是在 ES6 的 Proxy 出現之前,每個 Object 中呼叫的“方法”類似於一種字典查詢,foo.bar 總是去查詢 foo 物件中的“bar”屬性或者它的原型鏈。而比如在 Ruby 這種語言中,foo.bat 會發一條訊息 :bar 到 foo 物件中 – 這條訊息可以被攔截解析,它並不是必須做一個字典查詢。

Redux 是一種基於 JavaScript 已存在的物件系統上更慢和更復雜的物件系統,reducer 和 middleware 相當於儲存著狀態的 JavaScript 物件的攔截器和解析器。

作者:蘇靜顏
連結:http://www.jianshu.com/p/5a2e5a512fc5
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。





原文釋出時間為:2017年8月23日

本文來自雲棲社群合作伙伴掘金,瞭解相關資訊可以關注掘金網站。


相關文章