Redux 有多棒诅迷?
Redux 能夠優(yōu)雅地處理復(fù)雜且難以被 React 組件描述的狀態(tài)交互。它本質(zhì)上是一個消息傳遞系統(tǒng),就像在面向?qū)ο缶幊讨锌吹降哪菢颖愎螅皇?Redux 是通過一個庫而不是在語言本身中來實(shí)現(xiàn)的问顷。就像在 OOP 中那樣昂秃,Redux 將控制的責(zé)任從調(diào)用方轉(zhuǎn)移到了接收方 - 界面并不直接操作狀態(tài)值,而是發(fā)布一條操作消息來讓狀態(tài)解析杜窄。
一個 Redux store 是一個對象肠骆, reducers 是方法的處理程序,而 actions 是操作消息塞耕。store.dispatch({ type: "foo", payload: "bar" })
相當(dāng)于 Ruby 中的 store.send(:foo, "bar")
蚀腿。中間件的使用方式類似于面向切面編程 (AOP, Aspect-Oriented Programming) (例如:Rails 中的 before_action
)。 而 React-Redux 的 connect
則是依賴注入扫外。
為什么它值得稱贊莉钙?
- 上文中控制權(quán)限的轉(zhuǎn)移保證了當(dāng)狀態(tài)轉(zhuǎn)換的實(shí)現(xiàn)變化時, UI 并不需要更新筛谚。添加復(fù)雜的功能磁玉,例如記錄日志、撤銷操作驾讲,甚至是時光穿越調(diào)試 (time travel debugging)蚊伞,將變得非常簡單。集成測試只需要確認(rèn)派發(fā)了正確的 actions 即可吮铭,剩下的測試都可以通過單元測試來完成时迫。
- React 的組件狀態(tài)對于那些在 app 中觸及多個部分的狀態(tài)而言非常笨重,例如用戶信息和消息通知谓晌。Redux 提供了一個獨(dú)立于 UI 的狀態(tài)樹來處理這些交叉問題别垮。此外,讓你的狀態(tài)存活于 UI 之外使實(shí)現(xiàn)數(shù)據(jù)可持久化之類的功能變得更簡單 - 你只需要在一個單獨(dú)的地方處理 localStorage 和 URL 即可扎谎。
- Redux 的 reducer 提供了難以想象的靈活方式來處理 actions - 組合碳想,多次派發(fā),甚至
method_missing
式解析
這些都是不常見的情況毁靶。在常見情況下呢胧奔?
好吧,這就是問題所在预吆。
- 一個 action 可以被解釋為一個復(fù)雜的狀態(tài)轉(zhuǎn)換龙填,但是它們中的絕大對數(shù)只是用來設(shè)置一個單獨(dú)的值。Redux 應(yīng)用傾向于結(jié)束這一大堆只用于設(shè)置一個值的 action,這里有個用于區(qū)分在 Java 中手動寫 setter 函數(shù)的標(biāo)志岩遗。
- 你可以在你 app 的任意一個地方使用狀態(tài)樹的任一部分扇商,但是對于大多數(shù)狀態(tài)來說,它們一對一的對應(yīng)了某個 UI 中的一部分宿礁。將這種狀態(tài)放在 Redux 中案铺,而不是放在組件里,這只是間接而非抽象梆靖。
- 一個 reducer 函數(shù)可以做各種奇怪的元編程控汉,但是在絕大多數(shù)情況下它只是基于某個 action 類型的單一派發(fā)。這在 Elm 和 Erlang 這種語言中是很好實(shí)現(xiàn)的返吻,因?yàn)樵谶@些語言中姑子,模式匹配是簡潔而高效的,但是在 JavaScript 中使用
switch
語句來實(shí)現(xiàn)就顯得格外笨拙测僵。
但是更可怕的事是街佑,當(dāng)你花費(fèi)了所有的時間在常見情況下編寫代碼模板時,你會忘記捍靠,在某些特殊情況下會有更好的解決方案存在舆乔。你遇到了一個復(fù)雜的狀態(tài)轉(zhuǎn)換問題,然后調(diào)用了很多用于設(shè)置狀態(tài)值的 action 來解決了它剂公。你在 reducer 中重復(fù)定義了很多狀態(tài)希俩,而不是在 app 中分發(fā)同一個子狀態(tài)。你在很多 reducer 中復(fù)制粘貼了各種 switch case 而不是把其中的某些方法抽象成共有的方法纲辽。
這很容易把這種錯誤僅僅當(dāng)成 “操作員誤差” - 是他們沒有查看操作手冊颜武,就像可憐的工匠責(zé)怪他們手上的工具一樣 - 但是這種問題出現(xiàn)的頻率應(yīng)當(dāng)引起一些關(guān)注。如果大多數(shù)的人都錯誤的使用一款工具拖吼,那我們又該如何評價(jià)它呢鳞上?
所以我們應(yīng)該避免在常見情況下使用 Redux,而把它留給特殊情況嗎吊档?
這是 Redux 開發(fā)團(tuán)隊(duì)給你的建議篙议,也是我給我的開發(fā)團(tuán)隊(duì)成員的建議:除非使用 setState 難以解決問題,不然盡量避免使用 Redux怠硼。但是我不能讓我自己也遵從我自己的規(guī)定鬼贱,因?yàn)榭偸怯?strong>某些原因讓你想要使用 Redux。 可能你有一系列的 set_$foo
消息香璃,而且設(shè)置這些值也會更新 URL这难,或者重設(shè)某些瞬態(tài)值∑厦耄可能你有一些明確和 UI 一對一的狀態(tài)值姻乓,但是你也希望紀(jì)錄或者可以撤銷它們嵌溢。
事實(shí)是,我不知道如何寫蹋岩,更不要說指導(dǎo)寫“好的 Redux”赖草。我曾經(jīng)參與的每個 app 都充斥著 Redux 的反模式,因?yàn)槲蚁氩坏礁玫慕鉀Q方案或者我無法說服我的隊(duì)友來改變它剪个。如果一個 Redux “專家” 寫出來的代碼也如此平庸秧骑,那我們還能指望一個新手怎么做呢?無論如何禁偎,我只是希望能夠平衡一下現(xiàn)在大行其道的 “Redux 完成所有事” 解決方案腿堤,希望每個人都能在他們適用的情況下理解 Redux阀坏。
所以我們在這種情況下該怎么做呢如暖?
所幸的是,Redux 足夠靈活忌堂,我們可以使用第三方庫集成到 Redux 里來解決常見情況 - 例如 Jumpstate盒至。更清晰地說,我不認(rèn)為 Redux 專注于處理底層事務(wù)是一種錯誤的行為士修。但是將這些基礎(chǔ)的功能外包給第三方來完成會造成額外的認(rèn)知和開發(fā)負(fù)擔(dān) - 每個用戶都需要從這些部分里構(gòu)建自己的框架枷遂。
有些人執(zhí)著于此
而我正是其中之一。但并不是所有人都是棋嘲。個人而言酒唉,我愛 Redux,盡可能地使用它沸移,但是我仍舊喜愛嘗試新的 Webpack 設(shè)置痪伦。但是我并不代表絕大多數(shù)人群。我被實(shí)現(xiàn)靈活解決方案的心驅(qū)使著雹锣,在 Redux 的頂層寫了很多我自己的抽象方法网沾。但是看著那些一群六個月前就離職的、從來沒留下開發(fā)記錄的開發(fā)工程師所寫的抽象程序蕊爵,誰又能有動力呢辉哥?
其實(shí)很可能你根本不會遇到那些 Redux 特別擅長處理的難題,尤其如果你是一個團(tuán)隊(duì)里的新人攒射,這些問題基本上會交給更資深的工程師處理醋旦。你在 Redux 上累積的經(jīng)驗(yàn)就是 “用著每個人都在用的垃圾庫,把所寫的代碼都重復(fù)寫上好幾次”会放。 Redux 簡單到你可以不深入理解也能機(jī)械地使用它浑度,但是那是一種很無聊也沒什么提高的體驗(yàn)。
這讓我回想起了我之前提出的一個問題:如果大多數(shù)的人都在錯誤的使用一款工具鸦概,那我們又該如何評價(jià)它呢箩张?一個好的工具不僅僅應(yīng)該有用且耐用 - 它應(yīng)該讓使用者有個好的使用體驗(yàn)甩骏。能舒服使用它的場景就是正確的場景。一個工具的設(shè)計(jì)不僅僅是為了它要完成的任務(wù)先慷,同樣也要考慮到它的使用者饮笛。一個好的工具可以反映出工具制作者對于使用者的同情心。
那我們的同情心又在哪呢论熙?為什么我們的反應(yīng)總是 “你錯誤地使用了它” 而不是 “我們可以把它設(shè)計(jì)地更容易去使用” 呢福青?
這里有個函數(shù)式編程界的相關(guān)現(xiàn)象,我喜歡叫它 Monad 指南的詛咒:解釋它們是怎么工作的是非常簡單的脓诡,但是解釋清楚它們這么做是有意義的就出乎意料地困難了无午。
在這篇文章中你真的要讀到一段 monad 指南?
Moand 是一個在 Haskell 常見的開發(fā)模式祝谚,在計(jì)算機(jī)中的很多地方都被廣泛使用 - 列表宪迟,錯誤處理,狀態(tài)交惯,時間次泽,輸入輸出。這里有個語法糖席爽,你可以以 do
表達(dá)式的形式像輸入指令代碼一樣來輸入一系列的 monad 操作意荤,就好像 javascript 中的 generator 可以讓異步函數(shù)看起來像同步一樣。
第一個問題是只锻,用 monad 用來做什么來描述 monad 是不準(zhǔn)確的玖像。Haskell 曾引入 Monad 以解決副作用和順序計(jì)算,但是事實(shí)上 monad 作為一個抽象概念并不能解決副作用和順序化齐饮,它們是一系列規(guī)則捐寥,規(guī)定了一組函數(shù)如何交互,并沒有什么固定的含義沈矿。關(guān)聯(lián)性的概念適用于算術(shù)集合操作上真、列表合并和 null 傳播,但是它完全獨(dú)立于這些操作羹膳。
第二個問題是在一些小問題上睡互,用 monad 來解決問題更繁瑣了 - 至少看起來更復(fù)雜了 - 相比于指令式操作而言。給一個可選類型指定它的 Maybe Type
明顯比驗(yàn)證一個模糊的 null
類型更安全陵像,但是這又會讓代碼變得更難看就珠。使用 Either
類型來進(jìn)行錯誤處理通常比那些隨處可能 throw
錯誤的代碼更容易理解,但是 throw 操作的確比手動傳值更簡潔醒颖。而副作用 - 狀態(tài)妻怎,IO 等 - 在指令式語言中更是微不足道的。函數(shù)式編程愛好者們(包括我)會說副作用在函數(shù)式語言中太簡單了泞歉,但是讓別人相信任何一種語言很簡單本身就是一件很難的事逼侦。
而 monad 真正的價(jià)值只能在宏觀尺度體現(xiàn)出來 - 并不是這些用例都遵循著 monad 規(guī)則匿辩,但是這些用例都遵循著同樣的規(guī)則。能夠作用于一個用例的操作就可以作用于每個用例:把一對列表壓縮成一個存儲著對值的列表就和把一對 promise 函數(shù)融合成一個處理兩個結(jié)果的 promise 是“一樣的”榛丢。
所以呢铲球?
現(xiàn)在 Redux 有同樣的問題 - 它很難學(xué)習(xí)并不是因?yàn)樗茈y反而是因?yàn)樗?strong>簡單。理解并不是認(rèn)知的障礙晰赞,而要相信它的核心設(shè)計(jì)理念稼病,我們才能通過歸納來延伸其它的知識。
這種思想是很難共享的掖鱼,因?yàn)楹诵乃枷胧菬o趣的真理(避免副作用)或者做一些無意義的抽象((prevState, action) => nextState
)然走。任何單獨(dú)的例子都不會對這種理解有任何幫助,因?yàn)檫@些例子只是展示了 Redux 的細(xì)節(jié)但并不能展現(xiàn)它的核心思想戏挡。
一旦我們開始?接受別人的思想?芍瑞,我們中的很多人就會立刻忘掉自己之前的一些想法。我們忘記了我們的理解只能從我們自己一次又一次的失敗和誤解中獲得增拥。
所以你的建議是啄巧?
我覺得我們應(yīng)該承認(rèn)我們遇到了這個問題寻歧。Redux 是一種簡單卻不容易的語言掌栅。這是一種可以理解的設(shè)計(jì)選擇,但是仍舊是一種權(quán)衡码泛。對于一門犧牲了某些簡單性來讓它更便于使用的語言猾封,還是有很多人都會從中獲益的。但是噪珊,很多大型社區(qū)甚至不覺得這是一種已經(jīng)做出的權(quán)衡晌缘。
我認(rèn)為對比 React 和 Redux 是一件很有意思的事,因?yàn)閺V泛來說 React 是更復(fù)雜的痢站,它有著明顯更多 API 接口磷箕,同時它也在某種意義上更容易使用和理解。而 React 唯一必須的 API 接口是 React.createElement
和 ReactDOM.render
- 狀態(tài)阵难,組件生命周期岳枷,甚至 DOM 事件可以在別的地方處理。React 中的這些特性讓它變得更復(fù)雜呜叫,但是也讓它變得更出色空繁。
“原子化狀態(tài)”是個抽象概念,在你理解它之后可以指導(dǎo)你的開發(fā)朱庆,但是不管你理不理解這個概念盛泡,你都可以在 React 組件中調(diào)用 setState
,來實(shí)現(xiàn)原子化狀態(tài)管理娱颊。這并不是一個完美的解決方案 - 徹底替換狀態(tài)或者強(qiáng)制更新有著比它更高的效率傲诵,而且它是一個異步調(diào)用的方法還會產(chǎn)生一些 bug - 但是 React 將 setState
作為一個調(diào)用的方法而不是一個專業(yè)術(shù)語是一個很好的做法凯砍。
Redux 的開發(fā)組和社區(qū)都強(qiáng)烈反對增加 Redux 的 API 數(shù)量,但是現(xiàn)在將一堆小型開發(fā)庫融合在一起的做法對于專家而言是乏味的拴竹,而對于新手而言是費(fèi)解的果覆。如果 Redux 不能內(nèi)置一些小功能來對常見情況做一些支持,那么我們需要一個“更好”的框架在常見情況下來取代它殖熟。Jumpsuit 可以作為一個不錯的開始 - 它將“action”和“state”的概念轉(zhuǎn)化為了可調(diào)用的方法局待,同時保留了它們多對多的特性 - 但是事實(shí)上,這個庫其實(shí)并不關(guān)心這個優(yōu)化本身菱属。
諷刺的是:Redux 存在的意義 是“開發(fā)者體驗(yàn)”:Dan 建立了 Redux 因?yàn)樗M斫夂椭亟?Elm 的時光穿越調(diào)試钳榨。但是隨著它開發(fā)了它自己的特性 - 進(jìn)入了 React 生態(tài)系統(tǒng)的 OOP 運(yùn)行環(huán)境 - 它犧牲了一些開發(fā)者的體驗(yàn)以換取可配置性。這讓 Redux 得以蓬勃發(fā)展纽门,但是這是個人性化開發(fā)框架明顯的缺失薛耻。我們,Redux 社區(qū)赏陵,準(zhǔn)備好了嗎饼齿?
感謝 Matthew McVickar, a pile of moss, Eric Wood, Matt DuLeone, 和 Patrick Thomson review 本文。
備注:
[1] 為什么要在 React / JS 和 OOP 之間做明顯的區(qū)分蝙搔?JavaScript 是面向?qū)ο蟮穆聘龋遣皇腔陬悾╟lass-based)的。
OOP 類似于函數(shù)式編程吃型,是一種方法证鸥,不是某個語言特性。有些語言對于 OOP 支持地特別好勤晚,或者有一些專門為 OOP 定制的標(biāo)準(zhǔn)庫枉层,但是如果你對它的了解夠深,你可以用任何語言寫出面向?qū)ο箫L(fēng)格的代碼赐写。
JavaScript 有一種數(shù)據(jù)類型 Object鸟蜡,同時 JS 中大多數(shù)數(shù)據(jù)類型可以以 Object 的形式來處理和解析,從這種角度來說你可以對任何數(shù)據(jù)類型調(diào)用某些同樣的方法挺邀,除了 null
和 undefined
揉忘。但是在 ES6 的 Proxy 出現(xiàn)之前,每個 Object 中調(diào)用的“方法”類似于一種字典查找悠夯,foo.bar
總是去查找 foo 對象中的“bar”屬性或者它的原型鏈癌淮。而比如在 Ruby 這種語言中,foo.bat
會發(fā)一條消息 :bar
到 foo 對象中 - 這條消息可以被攔截或解析沦补,它并不是必須做一個字典查找乳蓄。
Redux 是一種基于 JavaScript 已存在的對象系統(tǒng)上更慢和更復(fù)雜的對象系統(tǒng),reducer 和 middleware 相當(dāng)于保存著狀態(tài)的 JavaScript 對象的攔截器和解析器夕膀。
掘金翻譯計(jì)劃 是一個翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū)虚倒,文章來源為 掘金 上的英文分享文章美侦。內(nèi)容覆蓋 Android、iOS魂奥、React菠剩、前端、后端耻煤、產(chǎn)品具壮、設(shè)計(jì) 等領(lǐng)域,想要查看更多優(yōu)質(zhì)譯文請持續(xù)關(guān)注 掘金翻譯計(jì)劃哈蝇、官方微博棺妓、知乎專欄。