一.作用
與 Flux 一樣,作為狀態管理層,對單向資料流做強約束
二.出發點
MVC 中,資料(Model)、表現層(View)、邏輯(Controller)之間有明確的界線,但資料流是雙向的,在大型應用中尤其明顯。一個變化(使用者輸入或者內部介面呼叫)可能會影響應用的多處狀態,例如雙向資料繫結,很難維護除錯
一個 model 可以更新另一個 model 的話,一個 view 更新一個 model,這個 model 更新了另一個 model,可能會引發另一個 view 的更新。不知道某一時刻應用到底發生了什麼,因為不知道何時、為何、怎樣發生的狀態變化。系統不透明,很難重現 bug 和添加新特性
希望透過強制單向資料流來降低複雜度,提升可維護性和程式碼可預測性
三.核心理念
Redux 用一棵不可變狀態樹維護整個應用的狀態,無法直接改變,發生變化時,透過 action 和 reducer 建立新的物件,具體如下:
-
應用的狀態物件沒有
setter,不允許直接修改 -
透過
dispatch action來修改狀態 -
透過
reducer把action和state聯繫起來 -
由上層
reducer把下層的組織起來,形成reducer樹,逐層計算得到state
函式式的 reducer 是關鍵:
-
小(職責單一)
-
純(沒有副作用,不影響環境)
-
獨立(不依賴環境,固定輸入對應固定輸出。容易測試,只用關注給定輸入對應的返回值是否正確)
純函式約束讓一些強大的除錯特性得以實作(否則狀態回滾幾乎是不可能的),透過 DevTools 精確追蹤變化:
-
顯示當前
state、歷史action及對應的state -
跳過某些
action,快速組合出 bug 場景,不需要手動準備 -
狀態重置(Reset),提交(Commit),回滾(Revert)
-
熱加載,定位
reducer問題,立即修改生效
四.結構
action 与Flux一样,就是事件,带有type和data(payload)
同样手动dispatch action
---
store 与Flux功能一样,但全局只有1个,实现上是一颗不可变的状态树
分发action,注册listener。每个action经过层层reducer得到新state
---
reducer 与arr.reduce(callback, [initialValue])作用类似
reducer相当于callback,输入当前state和action,输出新state
reducer 的概念相當於 node 中介軟體,或者 gulp 外掛程式,每個 reducer 負責狀態樹的一小部分,把一系列 reducer 串聯起來(把上一個 reducer 的輸出作為當前 reducer 的輸入),得到最終輸出 state
reducer 每次對 state 的修改,都會建立一個新的 state 物件,舊值指向原參照,新值被建立出來
嚴格的單向資料流:
call new state
action --> store ------> reducers -----------> view
action 也是交給頂層的所有 reducer(與 Flux 類似),流向相應子樹
store 負責協調,先把 action 和當前 state 傳遞給 reducer 樹,得到新 state,更新當前 state,再通知視圖更新(React 的話就是 setState())
action
action 負責描述發生了什麼(就像新聞標題)
action 與 action creator 分別對應傳統的 event 和 createEvent()。需要 action creator 是為了可移植和可測試
設計上把 action creator 和 store 分離是考慮伺服器端渲染,這樣每個請求對應獨立 store,由外部做 action creator 和 store 的繫結
注意:實踐中應該把建立 action 和 dispatch action 解開,在需要的場景(比如傳遞給子元件,希望遮蔽 dispatch),Redux 提供了 bindActionCreators 再把它們兩個綁起來
另外,考慮非同步場景:
action數量
一個非同步操作可能需要 3 個 action(或者 1 個帶有 3 種狀態的 action),開始/成功/失敗,對應的 UI 狀態為顯示 loading/隱藏 loading 並顯示新資料/隱藏 loading 並顯示錯誤資訊
- 更新
view的時機
非同步操作結束後,dispatch action 修改 state,更新 view
不用考慮多個非同步操作的時序問題,因為從 action 歷史記錄來看,順序是固定不變的,同步還是非同步過程中 dispatch 的不重要
與同步場景沒太大區別,只是 action 多一些,一些中介軟體(redux-thunk、redux-promise 等等)只是讓非同步控制形式上更優雅,從 dispatch action 角度看沒有區別
reducer
負責具體的狀態更新(根據 action 更新 state,讓 action 的描述成為事實)
相比 Flux,Redux 用純函式 reducer 來代替 event emitter:
- 分解與組合
透過拆分 reducer 來分解狀態,再把 reducer 組合起來(combineReducers() 工具函式)形成狀態樹,reducer 組合在 Redux 應用裡很常見(基本套路)
通常把 1 個 reducer 拆成一組相似的 reducer(或者抽象出 reducer factory)
- 單一職責
每一個 reducer 只負責全域狀態的一部分
純函式 reducer 的具體約束(與 FP 中的純函式概念一致)如下:
-
不修改參數
-
只是單純的計算,不要摻雜副作用,比如路由切換之類的其它 API 呼叫
-
不要呼叫不純(輸出不單取決於輸入,還與環境有關)的方法 比如
Math.random()、new Date()
另外,reducer 與 state 密切相關,state 是 reducer 樹的計算結果,所以需要先規劃整個應用的 state 結構,有一些非常好用的技巧:
- 把
state分為資料狀態和 UI 狀態
UI 狀態可以維護在元件內部,也可以掛到狀態樹上,但都應該考慮區分資料狀態和 UI 狀態
(簡單場景及 UI 狀態變化可能不需要作為 store 的一部分,而應該在元件級來維護)
- 把
state看做資料庫
對於複雜的應用,應該把 state 當做資料庫,存放資料時建立索引,關聯資料之間透過 id 來參照。這樣相對獨立,可以減少巢狀狀態(巢狀狀態會讓 state 子樹越來越大,而 資料表 + 關係表 就不會)
Store
膠水,用來組織 action 和 reducer,並支援 listener
負責 3 件事:
-
持有
state,支援讀寫(getState()讀,dispatch(action)寫) -
接到
action時,排程reducer -
註冊/解綁
listener(每次狀態變化時觸發)
五.3個基本原則
整個應用對應一棵 state 樹
這樣很容易生成另外一份 state(保留歷史版本),也很容易實作 redo/undo
state 只讀
-
只能透過觸發
action來更新state -
集中變更,且以嚴格順序發生(沒有需要特別小心的競爭條件)
-
而
action都是純物件,可以記錄日誌、序列化,存起來以後還能回放(除錯/測試)
reducer 都是純函式
輸入 state 和 action,輸出新 state。每次都返回新的,不維護(修改)輸入的 state
所以能隨便調整 reducer 執行順序,放電影一樣的除錯控制得以實作
六.react-redux
Redux 與 React 沒有任何關係,Redux 作為狀態管理層可以配合任何 UI 方案使用,例如 backbone、angular、React 等等
react-redux 用來處理 new state -> view 的部分,也就是說,新 state 有了,怎樣同步視圖?
container
也有 container 和 view 的概念(與 Flux 相同)
container 是一種特殊的元件,不含視圖邏輯,與 store 關係緊密。從邏輯功能上看就是透過 store.subscribe() 讀取狀態樹的一部分,作為 props 傳遞給下方的普通元件(view)
connect()
一個看起來很神奇的 API,主要做 3 件事:
-
負責把
dispatch和state資料作為props注入下方普通元件 -
往虛擬 DOM 樹自動插入一些
container -
內建效能優化,避免不必要的更新(內建
shouldComponentUpdate)
七.Redux 與 Flux
相同點
-
把 Model 更新邏輯單獨提出來作為一層(Redux 的
reducer,Flux 的store) -
都不允許直接更新
model,而要求用action描述每一個變化 -
(state, action) => state的基本思路是一致的
不同點
- Redux 是一種具體實作,而 Flex 是一種模式
Redux 只有一個,而 Flux 有 十好幾種實作
- Redux 的
state是 1 棵樹
Redux 把應用狀態掛在 1 棵樹上,全域只有一個 store
而 Flux 有多個 store,並把狀態變更作為事件廣播出去,元件透過訂閱這些事件來同步當前狀態
- Redux 沒有
dispatcher的概念
因為依賴純函式,而不是事件觸發器。純函式可以隨便組合,不需要額外管理順序
在 Flux 裡 dispatcher 負責把 action 傳遞給所有 store
- Redux 假設不會手動修改
state
道德約束,不允許在 reducer 裡修改 state(可以添新屬性,但不允許修改現有的)
不作為強約束是考慮某些效能場景,技術上可以透過寫不純的 reducer 來解決
如果 reducer 不純的話,依賴純函式組合特性的強大除錯功能會被破壞,所以強烈不建議這麼做
不強制 state 用不可變的資料結構,是出於效能(不可變相關的額外處理)和靈活性(可以配合 const、immutablejs 等使用)考慮
八.問題與思考
1.state 變化訂閱機制的粒度控制是怎樣的?
subscribe(listener) 只能得到全域完整 state,那麼 React setState() 粒度是怎樣的,怎麼分子樹?
手動處理。state 樹有任何變化都通知所有 listener,listener 裡手動判斷自己關注的那一小部分 state 變了沒。也就是訂閱機制不管分發,需要手動分發
2.react-redux 的 <Provider> 是怎麼回事?
猜一下,應該是透過 (猜錯了)所以要求在 hostContainerInfo 完成的黑魔法。render root 時把 Provider 作為頂層容器:
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
hostContainerInfo 長這樣子:
function ReactDOMContainerInfo(topLevelWrapper, node) {
var info = {
_topLevelWrapper: topLevelWrapper,
_idCounter: 1,
_ownerDocument: node ? node.nodeType === DOC_NODE_TYPE ? node : node.ownerDocument : null,
_node: node,
_tag: node ? node.nodeName.toLowerCase() : null,
_namespaceURI: node ? node.namespaceURI : null
};
if ("development" !== 'production') {
info._ancestorInfo = node ? validateDOMNesting.updatedAncestorInfo(null, info._tag, null) : null;
}
return info;
}
(摘自 ReactDOM v15.5.4 原始碼)
虛擬 DOM 樹上所有元件共享 hostContainerInfo,所以 store 在所有 container 裡都能存取,範例程式碼見 Usage with React
react-redux 真實實作
猜錯了,直接看吧
內部實例是私有屬性(一個隨機的 key,__reactInternalInstance&<random>),所以元件無法存取 hostContainerInfo,但是 React 提供了一個增強版 hostContainerInfo,叫 context,專門應對需要深層手動傳遞 props 的場景,大致是這樣:
// Provider
class Provider extends React.Component {
constructor(props) {
super(props);
}
// 把顶层手动传入的store prop作为context属性
getChildContext() {
return {store: this.props.store};
}
render() {
return this.props.children;
}
}
// container
class Container extends React.Component {
// 把context里的store取出来,作为container的prop
// container里就可以通过this.props.store访问store了
getDefaultProps() {
return {
store: this.context.store;
}
}
}
用起來就像 store 從頂層穿透到了所有元件,那麼,技術上在普通元件(view,非 container)裡也可以透過 this.context.store 直接存取 store(因為 context 會向下無腦自動傳遞,無法控制),但這樣做不太道德
P.S.一直不知道 context 有什麼用,終於明白了
3.樹的場景(無限級展開)怎麼處理?
一個典型的業務場景,無限級樹結構,處理技巧在於把 state 看做資料庫(前面提到過這個技巧)
按照 Redux 的理念,應該把 tree 打平成 nodes,粗粒度可以是 nodeId - children,細粒度就是 nodeId - node(children 變成了 childrenIdList,再查總 id 表得到 children)
打平能夠解決問題,比巢狀狀態好維護得多,如果樹元件對應一個 tree 物件的話(node 都在 tree 上),對一棵大樹做局部更新會很難受
P.S.3NF 竟然能應用在前端,簡直難以置信!
參考資料
-
Redux doc:非常棒的文件,讀起來根本停不下來
暫無評論,快來發表你的看法吧