Redux 也是我列在 THE LAST TIME 系列中的一篇,由於現在正在著手探究關於我目前正在開發的業務中狀態管理的方案。所以,這裡打算先從 Redux 中學習學習,從他的狀態中取取經。畢竟,成功總是需要站在巨人的肩膀上不是。
話說回來,都 2020 年了還在寫 Redux 的文章,真的是有些過時了。不過呢,當時 Redux 孵化過程中一定也是回頭看了 Flux、CQRS、ES 等。
本篇先從 Redux 的設計理念到部分源碼分析。下一篇我們在注重說下 Redux的 Middleware工作機制。至於手寫,推薦磚家大佬的:完全理解 redux(從零實現一個 redux)
ReduxRedux 並不是什麼特別 Giao 的技術,但是其理念真的提的特別好。
說透了,它就是一個提供了 setter、getter 的大閉包,。外加一個 pubSub。。。另外的什麼 reducer、middleware 還是 action什麼的,都是基於他的規則和解決用戶使用痛點而來的,僅此而已。下面我們一點點說。。。
設計思想在 jQuery 時代的時候,我們是「面向過程開發」,隨著 react 的普及,我們提出了狀態驅動 UI 的開發模式。我們認為:「Web 應用就是狀態與 UI 一一對應的關係」。
但是隨著我們的 web 應用日趨的複雜化,一個應用所對應的背後的 state 也變的越來越難以管理。
而 「Redux 就是我們 Web 應用的一個狀態管理方案」。
一一對應如上圖所示,store 就是 Redux 提供的一個狀態容器。裡面存儲著 View 層所需要的所有的狀態(state)。每一個 UI 都對應著背後的一個狀態。Redux 也同樣規定。一個 state 就對應一個 View。只要 state 相同,View 就相同。(其實就是 state 驅動 UI)。
為什麼要使用 Redux如上所說,我們現在是狀態驅動 UI,那麼為什麼需要 Redux 來管理狀態呢?react 本身就是 state drive view 不是。
原因還是由於現在的前端的地位已經愈發的不一樣啦,前端的複雜性也是越來越高。通常一個前端應用都存在大量複雜、無規律的交互。還伴隨著各種異步操作。
任何一個操作都可能會改變 state,那麼就會導致我們應用的 state 越來越亂,且被動原因愈發的模糊。我們很容易就對這些狀態何時發生、為什麼發生、怎麼發生而失去控制。
如上,如果我們的頁面足夠複雜,那麼view 背後state 的變化就可能呈現出這個樣子。不同的 component 之間存在著父子、兄弟、子父、甚至跨層級之間的通信。
而我們理想中的狀態管理應該是這個樣子的:
單純的從架構層面而言,UI 與狀態完全分離,並且單向的數據流確保了狀態可控。
而 Redux 就是做這個的!
下面簡單介紹下 Redux 中的幾個概念。其實初學者往往就是對其概念而困惑。
store❝保存數據的地方,你可以把它看成一個容器,整個應用只能有一個Store。
❞State❝某一個時刻,存儲著的應用狀態值
❞Action❝View 發出的一種讓 state 發生變化的通知
❞Action Creator❝可以理解為 Action 的工廠函數
❞dispatch❝View 發出 Action 的媒介。也是唯一途徑
❞reducer❝根據當前接收到的Action 和 State,整合出來一個全新的 State。注意是需要是純函數
❞三大原則Redux 的使用,基於以下三個原則
單一數據源單一數據源這或許是與 Flux 最大的不同了。在 Redux 中,整個應用的 state 都被存儲到一個object 中。當然,這也是唯一存儲應用狀態的地方。我們可以理解為就是一個 Object tree。不同的枝幹對應不同的 Component。但是歸根結底只有一個根。
也是受益於單一的 state tree。以前難以實現的「撤銷/重做」甚至回放。都變得輕鬆了很多。
State 只讀唯一改變 state 的方法就是 dispatch 一個 action。action 就是一個令牌而已。normal Object。
任何 state 的變更,都可以理解為非 View 層引起的(網絡請求、用戶點擊等)。View 層只是發出了某一種意圖。而如何去滿足,完全取決於 Redux 本身,也就是 reducer。
store.dispatch({
type:'FETCH_START',
params:{
itemId:233333
}
})
使用純函數來修改所謂純函數,就是你得純,別變來變去了。書面詞彙這裡就不做過多解釋了。而這裡我們說的純函數來修改,其實就是我們上面說的 reducer。
Reducer 就是純函數,它接受當前的 state 和 action。然後返回一個新的 state。所以這裡,state 不會更新,只會替換。
之所以要純函數,就是結果可預測性。只要傳入的 state 和 action 一直,那麼就可以理解為返回的新 state 也總是一樣的。
總結Redux 的東西遠不止上面說的那麼些。其實還有比如 middleware、actionCreator 等等等。其實都是使用過程中的衍生品而已。我們主要是理解其思想。然後再去源碼中學習如何使用。
源碼分析❝Redux 源碼本身非常簡單,限於篇幅,我們下一篇再去介紹compose、combineReducers、applyMiddleware
❞目錄結構Redux 源碼本身就是很簡單,代碼量也不大。學習它,也主要是為了學習他的編程思想和設計範式。
當然,我們也可以從 Redux 的代碼裡,看看大佬是如何使用 ts 的。所以源碼分析裡面,我們還會去花費不少精力看下 Redux 的類型說明。所以我們從 type 開始看
src/types看類型聲明也是為了學習Redux 的 ts 類型聲明寫法。所以相似聲明的寫法形式我們就不重複介紹了。
actions.ts類型聲明也沒有太多的需要去說的邏輯,所以我就寫注釋上吧
// Action的接口定義。type 欄位明確聲明
export interface Action<T = any> {
type: T
}
export interface AnyAction extends Action {
// 在 Action 的這個接口上額外擴展的另外一些任意欄位(我們一般寫的都是 AnyAction 類型,用一個「基類」去約束必須帶有 type 欄位)
[extraProps: string]: any
}
export interface ActionCreator<A> {
// 函數接口,泛型約束函數的返回都是 A
(...args: any[]): A
}
export interface ActionCreatorsMapObject<A = any> {
// 對象,對象值為 ActionCreator
[key: string]: ActionCreator<A>
}
reducers.ts// 定義的一個函數,接受 S 和繼承 Action 默認為 AnyAction 的 A,返回 S
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined,
action: A
) => S
// 可以理解為 S 的 key 作為ReducersMapObject的 key,然後 value 是 Reducer的函數。in 我們可以理解為遍歷
export type ReducersMapObject<S = any, A extends Action = Action> = {
[K in keyof S]: Reducer<S[K], A>
}上面兩個聲明比較簡單直接。下面兩個稍微麻煩一些
export type StateFromReducersMapObject<M> = M extends ReducersMapObject<
any,
any
>
? { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }
: never
export type ReducerFromReducersMapObject<M> = M extends {
[P in keyof M]: infer R
}
? R extends Reducer<any, any>
? R
: never
: never上面兩個聲明,咱們來解釋其中第一個吧(稍微麻煩些)。
StateFromReducersMapObject 添加另一個泛型M約束M 如果繼承 ReducersMapObject<any,any>則走{ [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }的邏輯{ [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never } 很明顯,這就是一個對象,key 來自 M 對象裡面,也就是ReducersMapObject裡面傳入的S。key 對應的 value 就是需要判斷 M[P]是否繼承自 Reducer。否則也啥也不是infer 關鍵字和 extends 一直配合使用。這裡就是指返回 Reducer 的這個 State 「的類型」其他types 目錄裡面其他的比如 store、middleware都是如上的這種聲明方式,就不再贅述了,感興趣的可以翻閱翻閱。然後取其精華的應用到自己的 ts 項目裡面
src/createStore.ts不要疑惑上面函數重載的寫法~可以看到,整個createStore.ts 就是一個createStore 函數。
createStore三個參數:
reducer:就是 reducer,根據 action 和 currentState 計算 newState 的純 FunctionpreloadedState:initial Stateenhancer:增強store的功能,讓它擁有第三方的功能createStore 裡面就是一些「閉包函數的功能整合」。
INIT// A extends Action
dispatch({ type: ActionTypes.INIT } as A)這個方法是Redux保留用的,用來初始化State,其實就是dispatch 走到我們默認的 switch case default 的分支裡面獲取到默認的 State。
returnconst store = ({
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Extts 的類型轉換語法就不說了,返回的對象裡面包含dispatch、subscribe、getState、replaceReducer、[$$observable].
這裡我們簡單介紹下前三個方法的實現。
getStatefunction getState(): S {
if (isDispatching) {
throw new Error(
`我 reducer 正在執行,newState 正在產出呢!現在不行`
)
}
return currentState as S
}方法很簡單,就是 return currentState
subscribesubscribe的作用就是添加監聽函數listener,讓其在每次dispatch action的時候調用。
返回一個移除這個監聽的函數。
使用如下:
const unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
unsubscribe();function subscribe(listener: () => void) {
// 如果 listenter 不是一個 function,我就報錯(其實 ts 靜態檢查能檢查出來的,但!那是編譯時,這是運行時)
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 同 getState 一個樣紙
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://`Redux`.js.org/api/store#subscribelistener for more details.'
)
}
let isSubscribed = true
ensureCanMutateNextListeners()
// 直接將監聽的函數放進nextListeners裡
nextListeners.push(listener)
return function unsubscribe() {// 也是利用閉包,查看是否以訂閱,然後移除訂閱
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://`Redux`.js.org/api/store#subscribelistener for more details.'
)
}
isSubscribed = false//修改這個訂閱狀態
ensureCanMutateNextListeners()
//找到位置,移除監聽
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}「一句話解釋就是在 listeners 數據裡面添加一個函數」
再來說說這裡面的ensureCanMutateNextListeners,很多 Redux 源碼都麼有怎麼提及這個方法的作用。也是讓我有點困惑。
這個方法的實現非常簡單。就是判斷當前的監聽數組裡面是否和下一個數組相等。如果是!則 copy 一份。
let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}那麼為什麼呢?這裡留個彩蛋。等看完 dispatch 再來看這個疑惑。
dispatchfunction dispatch(action: A) {
// action必須是個普通對象
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// 必須包含 type 欄位
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// 同上
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 設置正在 dispatch 的 tag 為 true(解釋了那些判斷都是從哪裡來的了)
isDispatching = true
// 通過傳入的 reducer 來去的新的 state
// let currentReducer = reducer
currentState = currentReducer(currentState, action)
} finally {
// 修改狀態
isDispatching = false
}
// 將 nextListener 賦值給 currentListeners、listeners (注意回顧 ensureCanMutateNextListeners )
const listeners = (currentListeners = nextListeners)
// 挨個觸發監聽
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}方法很簡單,都寫在注釋裡了。這裡我們再回過頭來看ensureCanMutateNextListeners的意義
ensureCanMutateNextListenerslet currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function subscribe(listener: () => void) {
// ...
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
function dispatch(action: A) {
// ...
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
// ...
return action
}從上,代碼看起來貌似只要一個數組來存儲listener 就可以了。但是事實是,我們恰恰就是我們的 listener 是可以被 unSubscribe 的。而且 slice 會改變原數組大小。
所以這裡增加了一個 listener 的副本,是為了避免在遍歷listeners的過程中由於subscribe或者unsubscribe對listeners進行的修改而引起的某個listener被漏掉了。
最後限於篇幅,就暫時寫到這吧~
其實後面打算重點介紹的 Middleware,只是中間件的一種更規範,甚至我們可以理解為,它並不屬於 Redux 的。因為到這裡,你已經完全可以自己寫一份狀態管理方案了。
而 combineReducers也是我認為是費巧妙的設計。所以這些篇幅,就放到下一篇吧~
參考連結