《看漫畫,學 Redux》 —— A cartoon intro to Redux
原文:A cartoon intro to redux
譯文:《看漫畫,學 Redux》 —— A cartoon intro to Redux
譯者:jasonslyvia (原來作者也在知乎 @楊森 )
Flux 架構已然讓人覺得有些迷惑,而比 Flux 更讓人摸不著頭腦的是 Flux 與 Redux 的區別。Redux 是一個基於 Flux 思想的新架構方式,本文將探討它們的區別。
如果你還沒有看過這篇關於 Flux 的文章,你應該先閱讀一下。
譯者註:也可以參考這篇文章
為什麼要改變 Flux?
Redux 解決的問題和 Flux 一樣,但 Redux 能做的還有更多。
和 Flux 一樣,Redux 讓應用的狀態變化變得更加可預測。如果你想改變應用的狀態,就必須 dispatch 一個 action。你沒有辦法直接改變應用的狀態,因為保存這些狀態的東西(稱為 store)只有 getter 而沒有 setter。對於 Flux 和 Redux 來說,這些概念都是相似的。
那麼為什麼要新設計一種架構呢?Redux 的創造者 Dan Abramov 發現了改進 Flux 架構的可能。他想要一個更好的開發者工具來調試 Flux 應用。他發現如果稍微對 Flux 架構進行一些調整,就可以開發出一款更好用的開發者工具,同時依然能享受 Flux 架構帶給你的可預測性。
確切的說,他想要的開發者工具包含了代碼熱替換(hot reload)和時間旅行(time travel)功能。然而要想在 Flux 架構上實現這些功能,確實有些麻煩。
問題1:store 的代碼無法被熱替換,除非清空當前的狀態
在 Flux 中,store 包含了兩樣東西:
- 改變狀態的邏輯
- 當前的狀態
在一個 store 中同時保存這兩樣東西將會導致代碼熱替換功能出現問題。當你熱替換掉 store 的代碼想要看看新的狀態改變邏輯是否生效時,你就丟失了 store 中保存的當前狀態。此外,你還把 store 與 Flux 架構中其它組件產生關係的事件系統搞亂了。

解決方案
將這兩樣東西分開處理。讓一個對象來保存狀態,這個對象在熱替換代碼的時候不會受到影響。讓另一個對象包含所有改變狀態的邏輯,這個對象可以被熱替換因為它不用關心任何保存狀態相關的事情。

問題2:每次觸發 action 時狀態對象都被直接改寫了
時間旅行調試法的特性是:你能掌握狀態對象的每一次變化,這樣的話,你就能輕鬆的跳回到這個對象之前的某個狀態(想像一個撤銷功能)。
要實現這樣的功能,每次狀態改變之後,你都需要把舊的狀態保存在一個數組中。但是由於 JavaScript 的對象引用特性,簡單的把一個對象放進數組中並不能實現我們需要的功能。這樣做不能創建一個快照(snapshot),而只是創建了一個新的指針指向同一個對象。
所以要想實現時間旅行特性,每一個狀態改變的版本都需要保存在不同的 JavaScript 對象中,這樣你才不會不小心改變了某個歷史版本的狀態。

解決方案
當一個 action 需要 store 響應時,不要直接修改 store 中的狀態,而是將狀態拷貝一份並在這份拷貝的狀態上做出修改。

問題3:沒有合適的位置引入第三方插件
當你在寫一些調試性工具時,你希望它們能夠更加通用。一個使用該工具的用戶應該可以直接引入這個工具而不需要做額外的包裝或橋接。
要實現這樣的特性,Flux 架構需要一個擴展點。
一個簡單的例子就是日誌。比如說你希望 console.log() 每一個觸發的 action 同時 console.log() 這個 action 被響應完成後的狀態。在 Flux 中,你只能訂閱(subscribe) dispatcher 的更新和每一個 store 的變動。但是這樣就侵入了業務代碼,這樣的日誌功能不是一個第三方插件能夠輕易實現的。

解決方案
將這個架構的部分功能包裝進其他的對象中將使得我們的需求變得更容易實現。這些「其他對象」在架構原有的功能基礎之上添加了自己的功能。你可以把這種擴展點看做是一個增強器(enhancers)或者高階對象(higher order objects),亦或者中間件(middleware)。
此外,使用一個樹形結構來組織所有改變狀態的邏輯,這樣當狀態發生改變的時候 store 只會觸發一個事件來通知視圖層(view),而這一個事件會被整棵樹中的所有邏輯處理(譯者註:「處理」不代表一定會改變狀態,這些改變狀態的邏輯本質上是函數,函數內部會根據 action 的類型等來確定是否對狀態進行改變)。

*注意:就上述這些問題和解決方案來說,我主要在關注開發者工具這一使用場景。實際上,對 Flux 做出的這些改變在其他場景中也非常有幫助。在上述三點之外,Flux 和 Redux 還有更多的不同點。比如,相比於 Flux,Redux 精簡了整個架構的冗餘代碼,並且復用 store 的邏輯變得更加簡單。這裡有一個 Redux 優點的列表可供參考。
那麼讓我們來看看 Redux 是怎麼讓這些特性變為現實的。
新的角色
從 Flux 演進到 Redux,整個架構中的角色發生了些許的變化。
Action creators
Redux 保留了 Flux 中 action creator 的概念。每當你想要改變應用中的狀態時,你就要 dispatch 一個 action,這也是唯一改變狀態的方法。
就像我在這篇關於 Flux 的文章中提到的一樣,我把 action creator 看做是一個報務員(負責發電報的人,telegraph operator),你找到 action creator 告訴他你大致上想要傳達什麼信息,action creator 則會把這些信息格式化為一種標準的格式,以便系統中的其他部分能夠理解。
與 Flux 不同的是,Redux 中的 action creator 不會直接把 action 發送給 dispatcher,而是返回一個格式化好的 JavaScript 對象。
The store
我把 Flux 中的 store 描述為一種過度控制的官僚機制。不能簡單直接的修改狀態,而是要求所有的狀態改變都必須由 store 親自產生,還必須要經歷 action 分發那種套路。在 Redux 中,store 依然是這麼的充滿控制欲和官僚主義,但是又有些不一樣。

在 Flux 中,你可以擁有多個 store,每一個 store 都有自己的統治權。每個 store 都保存著自己對應的那部分狀態,以及所有修改這些狀態的邏輯。
而 Redux 中的 store 更喜歡將權力下放,而且不得不這麼做。因為在 Redux 中,你只能有一個 store……所以如果你打算像 Flux 那樣 store 完全獨立處理自己的事情,那麼 Redux 中的 store 將變得工作量巨大。
因此,Redux 中的 store 首先會保存整個應用的所有狀態,然後將判斷哪一部分狀態需要改變的任務分配下去。而以根 reducer(root reducer)為首的 reducer 們將會承擔這個任務。
你可能發現這裡好像沒有 dispatcher 什麼事,看起來有點越權,但 store 已經完全接管了 dispatch 相關的工作。
The reducers
當 store 需要知道一個 action 觸發後狀態需要怎麼改變時,他會去詢問 reducer。根 reducer 會根據狀態對象的鍵(key)將整個狀態樹進行拆分,然後將拆分後的每一塊子狀態傳到知道該怎麼響應的子 reducer 那裡進行處理。

我把 reducers 看做是有點格外熱衷複印的白領。他們不希望把任何事搞砸,因此他們不會修改任何傳遞給他們的文件。取而代之的是,他們會對這些文件進行複印,然後在複印件上進行修改。(譯者註:當然,當這些修改後的複印件定稿後,他們也不會再去修改這些複印件。)
這是 Redux 的核心思想之一。不直接修改整個應用的狀態樹,而是將狀態樹的每一部分進行拷貝並修改拷貝後的部分,然後將這些部分重新組合成一顆新的狀態樹。
子 reducers 會把他們創建的副本傳回給根 reducer,而根 reducer 會把這些副本組合起來形成一顆新的狀態樹。最後根 reducer 將新的狀態樹傳回給 store,store 再將新的狀態樹設為最終的狀態。
如果你有一個小型應用,你可能只有一個 reducer 對整個狀態樹進行拷貝並作出修改。又或者你有一個超大的應用,你可能會有若干個 reducers 對整個狀態樹進行修改。這也是 Flux 和 Redux 的另一處區別。在 Flux 中,store 並不需要與其他 store 產生關聯,而且 store 的結構是扁平的。而在 Redux 中,reducers 是有層級結構的。這種層級結構可以有若干層,就像組件的層級結構那樣。
The views: 智能組件(smart components)和木偶組件(dumb components)
Flux 擁有控制型視圖(controller views) 和常規型視圖(regular views)。控制型視圖就像是一個經理一樣,管理著 store 和子視圖(child views)之間的通信。
在 Redux 中,也有一個類似的概念:智能組件和木偶組件。(譯者註:在最新的 Redux 文檔中,它們分別叫做容器型組件 Container component 和展示型組件 Presentational component)智能組件的職責就像經理一樣,但是比起 Flux 中的角色,Redux 對經理的職責有更多的定義:
- 智能組件負責所有的 action。如果智能組件里包含的一個木偶組件需要觸發一個 action,智能組件會通過 props 傳給一個 function 給木偶組件,而木偶組件可以把這個 function 當做一個回調。
- 智能組件不能定義 CSS 樣式。
- 智能組件幾乎不會產生自己的 DOM 節點,他的工作是組織若干的木偶組件,由木偶組件來生成最終的 DOM 節點。
木偶組件不會直接依賴 action 的觸發,因為所有的 action 都會當做 props 傳下來。這意味著木偶組件可以被任何一個邏輯不同的 App 拿去使用。同時木偶組件也需要有一定的樣式來讓自己變得好看一些(當然你可以讓木偶組件接受某些 props 作為設置樣式的變數)。
視圖層綁定

要把 store 綁定到視圖上,Redux 需要一點幫助。如果你在使用 React,那麼你需要使用 react-redux。
視圖綁定工作有點像為組件樹服務的 IT 部門。IT 部門確保所有的組件都正確的綁定到 store 上,並處理各種技術上的細節,以確保餘下層級的組件對綁定相關的操作毫無感知。
視圖層綁定引入了三個概念:
- <Provider> 組件: 這個組件需要包裹在整個組件樹的最外層。這個組件讓根組件的所有子孫組件能夠輕鬆的使用 connect() 方法綁定 store。
- connect():這是 react-redux 提供的一個方法。如果一個組件想要響應狀態的變化,就把自己作為參數傳給 connect() 的結果(譯者註:connect() 返回的依然是一個函數),connect() 方法會處理與 store 綁定的細節,並通過 selector 確定該綁定 store 中哪一部分的數據。
- selector:這是你自己編寫的一個函數。這個函數聲明了你的組件需要整個 store 中的哪一部分數據作為自己的 props。
根組件

所有的 React 應用都存在一個根組件(root component)。他其實就是整個組件樹最外層的那個組件,但是在 Redux 中,根組件還要承擔額外的任務。
根組件承擔的角色有點像企業中的高管,他將整個團隊整合到一起來完成某項任務。他會創建 store,並告訴 store 使用哪些 reducers,並最終完成視圖層的綁定。
當完成整個應用的初始化工作後,根組件的就不再插手整個應用的運行過程了。每一次重新渲染(re-render)都沒有根組件什麼事,這些活兒都由根組件下面的子組件完成,當然也少不了視圖層綁定的功勞。
Redux 完成的運行流程
讓我們看看上述各個部分是怎樣組合成一個可以運行的應用的。
配置環節
應用中的不同部分需要在配置環節中整合到一起。
(1) *準備好 store。*根組件會創建 store,並通過 createStore(reducer) 方法告訴 store 該使用哪個根 reducer。與此同時,根 reducer 也通過 combineReducers() 方法組建了一隻向自己彙報的 reducer 團隊。

(2) 設置 store 和組件之間的通信。根組件將它所有的子組件包裹在 <Provider> 組件中,並建立了 Provider 與 store 之間的聯繫。
Provider 本質上創建了一個用於更新視圖組件的網路。那些智能組件通過 connect() 方法連入這個網路,以此確保他們能夠獲取到狀態的更新。

(3) 準備好 action callback。為了讓木偶組件更好的處理 action,智能組件可以用 bindActionCreators() 方法來創建 action callback。這樣做之後,智能組件就能給木偶組件傳入一個回調(callback)。對應的 action 會在木偶組件調用這個回調時被自動 dispatch。(譯者註:使用 bindActionCreators() 使得木偶組件無需關心 action 的 type 等信息,只用調用 props 中的某個方法傳入需要的參數作為 action 的 payload 即可)

數據流
現在我們的應用已經配置完成,用戶可以開始操作了。讓我們觸發一個 action,看看數據是怎樣流動的。

(1) 視圖發出了一個 action,action creator 將這個 action 格式化並返回。

(2) 這個 action 要麼被自動 dispatch(如果我們在配置階段使用了 bindActionCreators()),要麼由視圖層手動 dispatch。

(3) store 接受到這個 action 後,將當前的狀態樹和這個 action 傳給根 reducer。

(4) 根 reducer 將整個狀態樹切分成一個個小塊,然後將某一個小塊分發給知道怎麼處理這部分內容的子 reducer。

(5) 子 reducer 將傳入的一小塊狀態樹進行拷貝,然後在副本上進行修改,並最終將修改後的副本返回根 reducer。

(6) 當所有的子 reducer 返回他們修改的副本之後,根 reducer 將這些部分再次組合起來形成一顆新的狀態樹。然後根 reducer 將這個新的狀態樹交還給 store,store 再把自己的狀態置為這個最新的狀態樹。

(7) store 告訴視圖層綁定:「狀態更新啦」

(8) 視圖層綁定讓 store 把更新的狀態傳過來

(9) 視圖層綁定觸發了一個重新渲染的操作(re-render)

這就是我所理解的 Redux,希望對你有所幫助。
更多資源
- Redux 官方文檔
- Redux 官方文檔中文版
- The Evolution of Flux Frameworks
- Smart and Dumb Components
- The upsides of using Redux
- The downsides of using Redux
- 如何評價數據流管理架構 Redux?
推薦閱讀:
※我們所說的前端框架與庫的區別?
※Vue 插件編寫與實戰
※react許可證的問題是否意味著要轉技術棧了?
※Angular UI 框架:Element Angular 發布 0.0.4-alpha.3 版本


