from https://github.com/PeterKow/react-redux-devtools-training

從 source code 來看 Redux 更新 state 的運行機制

2015 最熱門的 flux-like 的架構莫過於 Dan Abramov 所提出的 Redux 架構,作者因為在 React Europe 提出這個架構一炮而紅,最後還被邀請去 facebook 工作,這篇文章希望從 source code的角度去看 Redux 更新 state 的運行機制,不會涉及太多 React 的概念以及 Redux API 的用法,也預設大家對 ES6 的 arrow function 跟 rest operator 有初步的了解,有興趣可以自行參照 ReactRedux 的官方文件。因為我還是前端菜鳥,這篇文章如果有理解錯誤還請各位路過大神多多指教。


官方文件用一句話簡單描述 Redux

Redux is a predictable state container for JavaScript apps

要清楚這句話必須要先了解 state 是什麼,我常用電燈開關跟房間來比擬 state 跟 UI 的關係,開關按了下去,房間就亮了,相反地關了開關,房間就變暗了,不需要重新操作整個房間的電路,只要改變開關就可以達到你要的效果,對於UI來說,state 變了 UI 就變了,前端很適合用有限狀態機來做為描述的模型,state 是可操作的進入點,可以是 boolean、string、array、object,或是任何可以被操作的變數,在 Reactive Programming 中,任何需要 UI 變化的時候去更動 state,透過 unidirectional data flow 去傳遞 state 改變以後產生的 result ,單一方向的資料流可以使得你的架構更清楚簡單,也容易預測跟測試組件的行為,過於複雜的 state 之間交互作用或者過多的 state 通常是 bug 的主要來源 (the root of evil),如同你必須操作很多的開關接很多的電路才能使的燈泡發亮一樣,因此盡量降低 state 的複雜度就是許多前端 Reactive Programming 架構的 best practice,Redux 就是一個去管理 state 變化的 library。Redux 的程式碼相當的精簡,壓縮完只有 2 KB,其實還蠻建議可以親自讀一讀看一下裡面的實作,會比許多抽象的描述更容易了解運作的機制。反倒是作為 Redux 跟 React 結合的 react-redux 的 source code 更為複雜一點。


State is the single source of truth in the Store

Redux 把所有的 state 都放到 createStore function 裡頭的 currentState 裡面,是一個 single source of truth 的概念,整個 app 的 state 都是統一來自同一個地方,有助於方便操作跟管理,而 state 的更新只有一種方式來進行,由 createStore 裡的 dispatch 去觸發 currentReducer 並把新的 state assign 給 currentState 。至於 createStore 裡面的 getState,subscribe 這裡就不多著墨了。

Part of createStore.js

CombineReducers and State

Redux 裡頭的 reducer 是一個接受 state 跟 action 的 function,根據不同的 action type,來決定是否 return 新的或是舊的 state。

Redux 裡頭可以有許多的 reducer,但是 createStore 只接受一個 reducer 傳入,因此要透過 combineReducers 把多個 reducer 合併成一個 reducer,傳入 createStore 裡面作為 currentReducer,由 dispatch 去觸發合併後的reducer 等同依次執行了合併前各個 reducer。

combineReducer 不只合併了reducer,也合併了每個 reducer 對應的 state,
store 裡面的 state 格式由 combineReducer 的參數格式決定。如果使用combineReducer 的格式如下:

combineReducers({
key1: reducer1,
key2: reducer2,
})

則 state 為

{
key1: state1, //state1 is the return state in reducer1
key2: state2, //state2 is the return state in reducer2
}

也可以不指定 key 直接傳入 reducer

combineReducers({
reducer1,
reducer2,
})

object 裡面直接置入 function 會把 function name 作為 key,因此其實就如同:

combineReducers({
name1: reducer1, //name1 is the function name of reducer1
name2: reducer2, //name2 is the function name of reducer2
})

要了解 combineReducers 如何處理多個 reducer ,必須要從 reduce 這個 Array 的 method 開始看起。

array.reduce((prevResult, next) => {
return result;
}, initialValue);

reduce 會從 array 第 0 項開始依序遍歷這個 array 的每個 item,callback function 會傳入上一次 callback 的回傳 result (prevResult) 跟下一項 item (next) 作為參數然後 return 出這次的 result,一開始的 prevResult 為 initialValue,若沒有指定 initialValue 則以第 0 項作為 initialValue,並改從 array 第 1 項開始遍歷,reduce 的回傳值就是最後 callback 的回傳值,更詳細的說明跟舉例可參考 MDN 的文件

Redux 中的 mapValues.js 是負責將 combineReducers 中含有多個 reducer 的object ({key: function}) 參數,透過 reduce 轉換成一個 state 的 object ({key: state}),由於 reduce 遍歷的性質,每一個經過 combine 並傳入 store 裡的 reducer 在 dispatch 的時候都會跑過一遍。

combineReducers.js 與 mapValue.js 的部分節錄如下:

大概讀完 createStore.js 跟 combineReducers.js 就可以幾近了解 Redux 核心是如何運作 state 更新的機制了。簡單歸納出幾個重點:

  • currentState is the single source of truth in createStore function.
  • dispatch 是更動 state 的唯一方式。
  • dispatch 只會觸發 createStore 裡面的 currentReducer。
  • 多個 reducer 可以透過 combineReducers function 合併成一個 reducer。
  • reducer 所對應的 state 也會因為 combineReducers 用 {key: state} 的方式和併成一個 object,key取決於你使用 combineReducers 的方式。
  • 觸發合併過後的 reducer,等同於依序執行合併前的多個 reducer。

以下額外介紹 Redux 額外的一些 api 實作:

ApplyMiddleWare

ApplyMiddleWare 是類似用 express 中 middleware 的概念,來讓從dispatch到 reducer 之間,可以做到更多的事情,譬如可以每一次 dispatch 觀察其 log ,或是 async 的 dispatch action。

ApplyMiddleWare 的實作使用了不少 higher order function 的概念,higher order function 乃是一個接受 function 作為參數的 function,並 return 一個新的 function,利用 higher order function 可以簡單且有彈性的對既有 function 增添功能。

ApplyMiddleWare 利用 higher order function的方式把 middleware 的 function 去包住原本 createStore function 裡面的 dispatch 加強其功能,回傳一個新的 createStore function。

ApplyMiddleware 的用法為:

let newCreateStore = applyMiddleWares(middlewares)(createStore);
let store = newCreateStore(reducer, initialState);

可以用兩個階段去理解,第一個括號表示 applyMiddleWare 接收了 middlewares function 作為參數,return 了一個接受 next function 作為參數的 function,第二個括號表示 return 的 function 接受了 createStore function 作為參數,並 return 了一個新的 createStore function。使用新的 createStore function 就可 create 出帶有 middleware 效果的 store.dispatch

BindActionCreators

這裡也是用到 higher order function 的作法,使用 actionCreator 如同使用dispatch(action) 一樣。通常用在不想把整個 store 都傳到 component 的時候,是個方便的 api ,有 syntactic sugar 的意味。