這不是源碼解讀哦!!!若是你但願看到源碼解析,那我想你隨便 google 一下就有不少啦,固然 Redux 的源碼自己也是簡單易懂。推薦直接閱讀~ 的源碼自己也是簡單易懂,歡迎直接查看源碼。php
做者: 趙瑋龍html
終於要更新了,第二篇文章就這樣在清明節前給你們趕出來,但願你也能有個能夠充實本身的假期。此次分享下這個已經很老的前端技術棧(相對於前端發展速度來看),說他老並無說他的設計理念老,而是說它已經有本身的歷史印記了。還記得第一次redux發版已是2015年6月的事情了,其實它也經歷了不少過往纔是如今咱們看到的樣子哦。。。前端
在深挖歷史以前先看看這個 lib 是幹嗎的,是時候仔細研讀下這個 Motivation 了,若是你尚未真正使用過Redux,那我建議你能夠看下文檔前四章節: 動機,原理,三個原則,以及和其餘理念的對比~react
若是你已經使用過能夠直接跳過到正片了,沒有用過的能夠聽我簡單在這裏囉嗦兩句~ 按照做者的理念,由於咱們日漸複雜的前端邏輯和代碼,而且愈來愈多的框架和庫使用這個概念git
UI=f(data)
複製代碼
包括前端spa的流行咱們日漸複雜的項目邏輯會有愈來愈難控制的 state, 也就是公式中的 data,而每每這些問題都來源於 data 自己可變的數據(mutation)和異步化(asynchronicity),做者把這兩種問題的混合效應類比了這個實驗曼妥思和可樂,我在我家廁所試過。。勸你千萬不要嘗試。。。結局每每是爆炸的局面!!!那麼這個lib就是用來規約這個狀態讓他可控的。(每每有人說Redux是一個全局狀態管理模塊,我我的以爲不盡然,它提供給咱們所謂的規約以致於讓咱們的狀態可控,它確實會維護一個惟一的狀態state,而且全部的data都在這裏,可是它是否是全局的卻不是必須的!)github
從 release 最先的0.2.0到如今的4.0.0咱們能夠看到做者 Dan gaearon(也是我我的比較欣賞的開源做者之一)的心路歷程,最先的 Redux 可不是如今的樣子哦(雖然我也是從3.1.0纔開始使用~)sql
最先的版本(遙想那時候 React 正在倡導本身的 Flux 單向數據流, Github 也有各類本身基於這個理念實現的類型庫) Redux 也是基於 Flux 理念去實現的,在1.0.0以前 Redux 自己還涵蓋了現在 react-redux 庫的內容,自行封裝了相似於 Connector, Provider 等高階組件去完成 Redux 和 React 的銜接。(做者的目的也顯而易見,但願你能無痛的在 React 中去使用 Redux)。那時候的 Flux 庫大多高舉 functional programming 的大旗,由於那時候這種向函數式編程借鑑的 Flux 概念自己也是這麼想的,咱們前面提到過讓一切數據流向包括邏輯可控,這偏偏是 functional programming 裏 prue function 的概念,這也和 React 當年作jsx語法的初衷一致。可是一旦拋出這樣的理論就須要你的受衆羣體去接受這個概念數據庫
+--------+ +------------+ +-------+ +----+
| Action | +----------->+ Dispatcher +----------->+ Store +----------->+View|
+--------+ +-------+----+ +-------+ +-+--+
^ |
| |
| |
| |
+----------------------------------------+
複製代碼
咱們能夠發現數據流向是單向的,這就是 Flux 核心理念,而 Redux 確實是遵循了這個理念可是又有些不一樣,不一樣在哪呢?編程
+----------+
| | +-----------+ sliceState,action +------------+
| Action |dispacth(action)| +--------------------------------> |
| +----------------> store | | Reducers |
| Creators | | <--------------------------------+ |
| | +-----+-----+ (state, action) => state +------------+
+-----^----+ |
| |
| |subscribe
| |
| |
| |
| +--------+ |
+--------------+ View <---+
+--------+
複製代碼
咱們能夠從圖中看到沒有了 Dispatcher 反而多了一個 Reducers,這裏不得不提一個點就是全部的 Flux 架構都圍繞着所謂 Predictable(可預測的) 的概念來維護 state, 那麼如何作到可預測的也就是咱們必須保證咱們的 state Immutable(數據不可變), Flux 裏依靠 dispatcher 來分發保證 Entity 的不可變性,而 Redux 中是依靠 pure function 的概念來保證每次的 state 都是原始 state 的一個快照, 也是這個核心公式的實踐 (state, action) => state 這樣你的 Reducers 其實就是這樣的任意多個 function,如何拆分這些 function 就是你須要考慮的事情了。而若是是 pure function 的話也利於咱們去作函數複用和單元測試。這就是 Redux 向函數式編程的概念借鑑的理念, 若是你熟悉 Elm 你必定知道 Model 的概念,要更新一個 Model 而且映射到 view 上你須要有 updater 去更新你的 Model,這裏 Redux 借鑑了 updater 的概念去作 reducers 拆分和複用。若是你對 Elm 也感興趣能夠看這裏。redux
其實函數式編程的理念也貫穿到了源碼中,好比裏面 compose 和 middleware 的實現,這些你均可以參考源碼,有意思的是其實縱使連原做者在一些函數式編程概念上也會有沒意識到的地方,在一些實現上也遵循了一些pr的意見,好比 compose 的實現:
從最先期的 reduceRight 改爲 reduce 這點就能發現,迭代了三個大版本和多個小版本的做者依然沒有意識到從右向左執行函數居然能夠不用 reduceRight,感興趣的同窗能夠試驗下,我當時看到這個pr也是驚訝這個提出者的 Lisp 或者 Haskell 功底啊,纔能有這樣的直覺!! (其實函數式編程確實是能夠鍛鍊邏輯思惟模式和你的數學意識,可是真的僅此而已,並不會在所謂性能和可讀性上帶來什麼明顯提高) 爲了功能的完整和解耦性,以前的版本嚴重耦合 React 也作出了調整,把上面提到的通訊高階組件單獨提到 react-redux 庫單獨維護了,這樣 Redux 自己也更加純淨的作狀態管理這件事。
在回顧了前世以後,咱們來看看現在的 Redux, 在基於多個版本的迭代和你們的實踐事後,不管是從概念自己仍是從最佳實踐的案例來看,包括 Github 上一些基於 Redux 作的封裝都已經有了默認的最佳實踐和使用規範,那咱們來看看今天的 Redux 自己使用的場景和方式。 從上面的理念咱們看出來如何拆分 reducer 和維護那個單一不可變 state 是咱們使用 redux 最應該關注的事情。 咱們下面主要說下在 React 中使用 Redux 的最佳實踐方式: (現實應用場景中,咱們現在大多數人應該仍是使用 Redux+React 的開發方式, 若是你仍是對於 Redux 是個初學者那麼你應該看這裏)。 爲了討論的具備必定的官方性,咱們按照官方文檔來看下(我會在我認爲比較我的的想作出備註和闡述), 着重討論如下三方面:
爲何先說 reducer 呢? 由於其實咱們的 state 都是 reducer 組成的, 上面那張圖能夠看出 (state, action) => state 是計算出 state 的規約公式, createStore() 這個 api 也是接受你的 reducer 來生成 state 的。 咱們先來看看最外層咱們須要爲 state 生成 Initinalizing state 方式:
// 官網說無非兩種方式
// 最外層你有一個reducer:
function rootReducer(state = 0, action) { // 在你的createStore第二個參數沒有的狀況下,你是須要給state一個默認值
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
default: return state;
}
}
// 經過官方提供的combineReducer去生成這個rootReducer, 其實你觀看源碼的話這個方法return的仍是一個 (state, action) => {}的函數
function a(state = 'zwl', action) { // 在你的createStore第二個參數沒有的狀況下,你是須要給state一個默認值
return state;
}
function b(state = 'zwt', action) { // 在你的createStore第二個參數沒有的狀況下,你是須要給state一個默認值
return state;
}
const rootReducer = combineReducers({ a, b })
複製代碼
既然初始化咱們看到上面提到的規約公式能夠初始化你的 state, 另外一個數據流向是反向的, reducer 會從你的 state 拿到須要處理的 sliceState,這裏就須要翻開書看看官方文檔是怎麼提這個所謂 state 的範式處理狀態的, 文檔會從三個地方提到這個 state 自己的規約處理,分別是
固然我以爲做者已經說的很清楚了,文章尾部也給了不少連接,可是這裏仍是有必要總結下這個規約的 state 範式化大概應該有些什麼最佳實踐:
// 首先先看下這裏的 state 基本結構,固然文檔中也沒有限制你,鼓勵你根據本身的業務形態去定製,可是倒是有些比較好的實踐方式
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
// 區分領域的數據, 而且可能會有兩種非領域數據類型,一種是頁面上一些ui狀態好比一個 button 是否展現的 boolean 值,這時候你會發現所謂的 sliceState 可能就是一個 domainData 或者是它下面的一個更小的分支,這個是根據你的 reducer 拆分規則指定的, 可是你能夠想象下若是你的 data 是單緯數據結構或者簡單數據結構,它就會很是好作邏輯計算,好比你有[a, b, c]單緯數組就比[{},{},{}]要好刪查改除!
{
domainData1: {},
domainData2: {},
appState1: {},
appState2: {},
ui: {
uiState1: {},
uiState2: {},
}
}
// 通過網絡上一些經驗包括筆者本身的經驗,你的基本數據類型每每會遵循一個數據原則爲了儘量維護最小的單元的數據,數據共享的部分會放在一塊兒維護,至於如何範式化這個 state 後面也會提到
{
domainData1: {},
domainData1ID: [],
domainData2: {},
domainData2ID: [],
entites:{ //這裏存放你須要共享數據的部分,可是僅僅是實例, 這裏的實例的引用每每放在外面, 遵循的原則是實例和引用分開而且若是實例裏有
//引用domainData1裏的東西那麼其實引用的也是id,你會存一個引用的id進去
commonData1: {},
commonData2: {},
},
commonData1ID: [],
commonData2ID: [],
ui: {
uiState1: {},
uiState2: {},
}
}
複製代碼
下面我來講下範式化 state 這個問題:
// 文檔中列舉了一個博客數據的例子(固然其實這個數據結構已經挺複雜的了)
const blogPosts = [
{
id: "post1",
author: {username: "user1", name: "User 1"},
body: "......",
comments: [
{
id: "comment1",
author: {username: "user2", name: "User 2"},
comment: ".....",
},
{
id: "comment2",
author: {username: "user3", name: "User 3"},
comment: ".....",
}
]
},
{
id: "post2",
author: {username : "user2", name : "User 2"},
body: "......",
comments: [
{
id: "comment3",
author: {username : "user3", name : "User 3"},
comment: ".....",
},
{
id: "comment4",
author: {username : "user1", name : "User 1"},
comment: ".....",
},
{
id: "comment5",
author: {username : "user3", name : "User 3"},
comment: ".....",
}
]
}
// 重複不少遍
]
// 其實這裏咱們能夠想象一下,若是咱們須要更新這個數據結構,假如說直接把這個數據掛在 state 上。 那就會出現這種狀況的代碼[...state, {...slice[comments], ...sliceUpdater}]或者嵌套更深的更新方式,首先咱們知道不管是擴展運算符和Object.assign都是淺拷貝,咱們每每須要對嵌套結構每個層級都去更新,若是操做數據結構就更加不方便了咱們須要根據每一個層級找到相應嵌套比較深的數據結構而後進行操做。這也就是爲何我前面說咱們儘可能維持單維度的數據結構緣由
// 文檔中建議咱們拍平數據後獲得這樣的數據結構
{
posts: {
byId: {
"post1": {
id: "post1",
author: "user1",
body: "......",
comments: ["comment1", "comment2"]
},
"post2": {
id: "post2",
author: "user2",
body: "......",
comments: ["comment3", "comment4", "comment5"]
}
}
allIds: ["post1", "post2"]
},
comments: {
byId: {
"comment1": {
id: "comment1",
author: "user2",
comment: ".....",
},
"comment2": {
id: "comment2",
author: "user3",
comment: ".....",
},
"comment3": {
id: "comment3",
author: "user3",
comment: ".....",
},
"comment4": {
id: "comment4",
author: "user1",
comment: ".....",
},
"comment5": {
id: "comment5",
author: "user3",
comment: ".....",
},
},
allIds: ["comment1", "comment2", "comment3", "commment4", "comment5"]
},
users: {
byId: {
"user1": {
username: "user1",
name: "User 1",
}
"user2": {
username: "user2",
name: "User 2",
}
"user3": {
username: "user3",
name: "User 3",
}
},
allIds: ["user1", "user2", "user3"] // 這裏官方推薦把相應的id放在層級內。這個地方其實均可以也能夠像我前面提到的放在users平級的地方,這個取決你的項目具體而定
}
}
// 能夠發現不一樣數據之間都被打成平級的關係,不須要去處理深層嵌套結構的問題,在給定的ID裏去刪查改除都比較方便!這裏更新的話也是不會波及到別的 domainComponent 好比咱們只是更新 users 裏的信息只須要去更新 users > byId > user 這部分去作淺複製,它不會像上面那種嵌套數據結構總體更新影響別的相應渲染組件也去更新,這裏其實還有一個優化點咱們後面會說,就是咱們在選擇這個 sliceState 的時候, 從選擇的 selector 不作重複運算。
複製代碼
這裏拍平方式建議採用Normalizr本身寫也不是不行,可是狀況會比較多,這個第三方庫仍是能比較好的解決這個問題。這裏再提一句這個 Normalizer 有一個 denormalize 方法便於你把 normaliz 的數據結構給裝回去。是否是感受有點像範式數據庫裏的 join 表的過程呢? 若是你熟悉範式化數據庫設計,你可能以爲這有一點點範式化數據庫的概念,只不過這裏確實是沒有嚴格的定義必須遵循第幾範式設計,它最重要的是你須要找到適合你的範式結構,這裏做者也在文檔中去給出一些連接(固然你不必先去學習數據庫的概念)能夠簡單瞭解下這些概念,包括多對多數據庫設計:
既然前面提到 sliceState 須要有個 selector,從 state 中選擇相應的 slice 這個分片(這裏順便把前面提到的小優化不須要作重複運算的 selector 也提一下,這裏會用到這個庫):
// 首先你的sliceState須要去state選擇相應的分片大多時候你都是
const usersSelector = state.users
const commonsSelector = state.commons
// 可是你會發現有些值是經過兩個selector計算而來的,咱們就拿reselect官網的第一個例子來看下
import { createSelector } from 'reselect'
const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent
const subtotalSelector = createSelector(
shopItemsSelector,
items => items.reduce((acc, item) => acc + item.value, 0)
)
const taxSelector = createSelector(
subtotalSelector,
taxPercentSelector,
(subtotal, taxPercent) => subtotal * (taxPercent / 100)
)
export const totalSelector = createSelector(
subtotalSelector,
taxSelector,
(subtotal, tax) => ({ total: subtotal + tax })
)
let exampleState = {
shop: {
taxPercent: 8,
items: [
{ name: 'apple', value: 1.20 },
{ name: 'orange', value: 0.95 },
]
}
}
console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState)) // 0.172
console.log(totalSelector(exampleState)) // { total: 2.322 }
// 這裏使用reselect的做用是若是下次傳入的shopItemsSelector,taxPercentSelector 並無改變那麼這個selector不會從新計算,這個你們有興趣能夠看下源碼,自己源碼也很少很容易看完!
複製代碼
上面概念裏提到了 selector 和 state 也能多少看到 state 自己只是可讀(read only)並不可修改, 下面我來講下咱們的函數 reducer 如何拆分,它的規約又是如何的(官方說有如下幾種 reducer):
具體定義你能夠參考這裏, 在我看來也不盡然非要分的這麼細,函數主要的做用仍是幫咱們拆分邏輯以及能達到複用的效果,因此拆分 reducer 纔是核心的概念。 具體的拆分邏輯能夠參考這裏,我這裏就不班門弄斧了,文檔的案例足夠清楚了。 這應該是我看到最上心的文檔了。不得不說做者是一個用心且勤奮的人!!!
咱們這裏就說一些特殊場景的 reducer 如何處理,固然文檔裏仍是說了在這裏如何處理須要跨分片數據的 reducer,通俗點講就是咱們須要 sliceStateA 的 reducer 須要處理 sliceStateB 裏的數據:
// 第一種方式
// (還記得咱們開頭說的 Initinalizing state 的方式嗎? 下面兩種方式就是利用這點)
function combinedReducer(state, action) { // 在 root 層去拿最外面的 state 把相應須要的 sliceState 傳給相應須要的 reducer
switch(action.type) {
case "A_TYPICAL_ACTION": {
return {
a: sliceReducerA(state.a, action),
b: sliceReducerB(state.b, action)
};
}
case "SOME_SPECIAL_ACTION": {
return {
// 明確地把 state.b 做爲額外參數進行傳遞
a: sliceReducerA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
}
}
case "ANOTHER_SPECIAL_ACTION": {
return {
a: sliceReducerA(state.a, action),
// 明確地把所有的 state 做爲額外參數進行傳遞
b: sliceReducerB(state.b, action, state)
}
}
default: return state;
}
}
// 第二種方式
const combinedReducer = combineReducers({
a: sliceReducerA,
b: sliceReducerB
});
function crossSliceReducer(state, action) {
switch(action.type) {
case "SOME_SPECIAL_ACTION": {
return {
// state.b是額外的參數
a: handleSpecialCaseForA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
}
}
default: return state;
}
}
function rootReducer(state, action) {
const intermediateState = combinedReducer(state, action);
const finalState = crossSliceReducer(intermediateState, action);
return finalState;
}
// 這都是官方推薦的方法, 可是你會發現萬變不離其中,都須要從根部 root 去拿 state 達到共享數據的方式,而且不管是 combineReducers 仍是 function 的方式都是要 Initinalizing state 的
複製代碼
最後再來簡單討論下異步化的問題,首先在早期 Redux 版本源碼裏是兼顧了異步方案的,就是咱們所熟悉的 redux-thunk 固然跟 react-redux 被整理出來單獨做爲項目同樣的,它也被單獨整理出來只是在文檔中說起了一下。其實市面上的基於 Redux 異步解決方案也很是多,解決不一樣場景的 redux-thunk 應該就夠了,可是還有很複雜的請求場景可能就須要下面兩個如今比較流行的庫去解決:
針對兩個方案沒有好壞之分,首先他們都解決了一樣的問題,可是兩個理念徹底不同。
用 Generator 去解決異步問題而且本身定義了不少 api 便於你解決各類複雜場景的異步問題 例如: [call, put, cancel,...] 不少種方法,關於這個 redux-saga 文論是官方文檔仍是網絡上的各類教程已經不少我就不在這廢話了。
採用了 rx.js 的方式去解決異步問題,而 rx.js 這個庫主要是 reactive programming 一種實現,它屬於 reactivex 其中一個分支利用流概念解決異步編程問題。這是一個很是大的話題,咱們有機會也會開專題來討論下這個 rx.js。雖然學習它自己會比 redux-saga 有更多的 api,可能還有一堆以前沒有接觸過的概念須要理解。可是就面向將來可能性上學習 rx.js 自己的價值確定會比 redux-saga 要有用的多。
不過筆者也會根據業務和團隊來決定這個問題比較合理,若是算上學習成本和開發成本可能自己 redux-saga 更加適合大型項目和多人維護團隊。因此具體哪一種方式更加適合你,就由你來定啦!
最後來看下官方推薦的一些項目目錄作法,你在這裏也能看到比較全的作法! 我比較推薦第一種作法: 分別定義 actions, reducers(裏面有相應的 selector), constants(actionTypes), components, containers 這樣我以爲比較清晰。 說了這麼多如今成熟的最佳實踐。是否是該暢想下將來呢?
其實我在上一篇文章中也提到了 React 自己的核心理念應該是會兼容單向數據流的方式(由於新的 context api 的存在!) 若是你不熟悉這個 context 能夠參考React blog 這裏我只是暢想下,僅表明我的觀點,不能表明將來任何發展趨勢。
// 上一篇文章咱們利用 context 去實現 react-redux 的時候咱們利用 context 傳遞了 redux 自己的 store,具體的 provider 和 connect 能夠參考上一篇文章
// 咱們本身實現的 store應該是這樣的。(所有憑自我意淫。。。能夠看個思路)
export const makeStore = (store) => {
let subscriptions = []
const Context = createContext()
const getState = () => store.initialState ? store.initialState : {} // 拿到當前的 state
const subscribe = fn => {
subscriptions = [...subscriptions, fn]
}
// 這裏把 Provider 和 Connect 拿進來,他們倆分別使保存這裏 store 和把 mapStateProps 以及 actions 傳遞進去
class Privider... // 一個維護Context.Provider 負責傳遞 store,更改store class Connect... // 一個負責消費的Context.Cousumer 傳遞給你的組件相應的state,和actions // 這裏我還沒想好如何維護總體代碼結構。。 } 複製代碼
在我準備發文章的時候,已經有人完成了這類庫,那我就只能安利一波了。但願你們能看到一個方向而不是全盤否決 Redux。 由於畢竟如今咱們尚未真正作好代替它的準備,並且我相信你若是真的要代替的話,在現有的項目和新項目可能都會有很多坑,不過俗話說得好不踩坑怎麼進步呢?(歡迎你們多多踩坑哈哈哈哈!!!!)
咱們經歷一門技術也好,經歷一個技術時代革新也罷。其實每每最重要的是過程,若是咱們忽略過程只在意結果那麼一切好像都是沒有調味的菜----索然無味了,再回歸到 Redux 自己,它給咱們帶來的最多的是一種規約(若是你跟着文章讀下來你應該會體會到!),如何在現在多人團隊的項目中儘可能增長可讀性和提升維護成本,也是工程化從來探討的主題。固然所謂的最佳實踐也不過是咱們真正實踐事後從不管是後端也好別的行業也好借鑑那些咱們真正有用的知識加以改造。所謂舉一反三的重要性吧!最後指望讀者還能繼續關注個人我的更新以及團隊更新!!!願在技術的浪潮中咱們共勉前行。