Redux 本來並不複雜,其基本理念能夠歸納爲:經過 action 提交變動,通過 reducer 計算出新的 state。action 是一個約定含 type 字段的對象,reducer 是一個約定以 state 和 action 爲參數、以新的 state 爲返回值的純函數。javascript
state -> dispatch(action) -> reducer -> new State => new View
複製代碼
考慮最原始的情形,在業務代碼中直接 dispatch action 對象,那麼只須要定義三個文件就能夠了:state.js、reducer.js、store.js,分別用於定義 state、reducer 和建立 store。html
業務代碼直接 dispatch action 對象並很差,一是重複,二是無類型校驗。官方推薦用函數來建立 action,而且 action 的 type 最好用常量而不是字符串,同時還需保證 type 的全局惟一性。因而再加兩個文件:actions.js、type.js,分別用於定義 action 函數和 type 常量。vue
這就造成了廣泛遭受詬病的模板代碼(常量還得全大寫,害),因而出現了一些方案,來簡化 action 與 type 的定義,好比 reduxsause、react-arc。java
官方僅僅約定了 reducer 的 interface,但沒有規定具體實現。原始案例中用 switch,每一個 case 對應一個 action,當 action 增多時,一個超長的 reducer 顯然是不利於維護的。官方提供了一種拆分思路,並提供了相應的輔助函數(combineReducers)。react
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
複製代碼
不過這種模式有不少問題,好比:每一個字段都須要定義一個子 reducer;子 reducer 的 state 參數都不同,而且不能忘了設置初始值;同一個 action 可能分佈在不一樣的子 reducer 中,每新增一個 action,若是對應多個字段的變動,那麼須要在多個 reducer 中新增 case 分支。git
redux 約定 state 必須是全局惟一而且是 immutable 的(約定,而非約束),reducer 每次都須要整個的返回一個新的 state。若是數據結構比較深,更新起來很麻煩。這個問題相對好解決,一般用 immutable-helper、seamless-immutable 之類的輔助庫便可。github
好像全部介紹 Redux 的文章都會不約而同的宣稱一個極具誤導性的論斷:Redux 不支持異步狀態更新。vuex
那是否是說,若是單純用 Redux,應用裏的異步邏輯就沒法寫了?顯然沒有這回事。typescript
// 照常寫異步邏輯,而後提交更新,有啥問題咩?
fetch().then(data => {
dispatch(updateAction(data))
})
複製代碼
不過有人會這麼想:我就想提交一個 action,由這個 action 去完成異步邏輯,人家 vuex 和 mobX 都有相關的支持,redux 咋就不行?redux
因而官方經過 redux-thunk 提供了一個語法糖,讓你能夠 dispatch 一個封裝了異步邏輯的 thunk 函數,使得代碼在語義上可以實現「提交了一個異步 action」這麼個事兒。
// 寥寥數行的 redux-thunk 中間件
function createThunkMiddleware() {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') { // 啊哈
return action(dispatch, getState)
}
return next(action)
};
}
複製代碼
看清本質你就會明白,哪裏有「redux 不支持異步狀態更新」這回事兒呢?只是代碼邏輯聚合在哪裏的區別而已,最終要觸發 reducer 更新 state,仍是得在 dispatch 一個對象的時候。因此根本不存在「非得引一個庫 redux 才能支持異步」這回事兒,不管是 redux-thunk、redux-promise 仍是 redux-saga,與其說在彌補 redux 的缺陷,倒不如說它們在解決本身額外創造出來的問題。
偏激的講,【async action】 是一個與 redux 無關的概念陷阱。客觀的說,【async action】是一種代碼設計抽象。原本 action 就是一個純對象而已,如今 action 還能夠是一個函數,被稱爲異步 action。
const a = { type, payload } // 這是一個 reducer action
const b = payload => ({ type, payload }) // 這也是一個 reducer action
const c = dispatch => fetch().then(res => dispatch(xxx) // 這是一個 async action
複製代碼
任何狀態管理庫,提供的所謂異步狀態更新功能,都只是一種 api 層面上的包裝。這固然有好處,好比提供了額外的抽象約束,狀態更新邏輯更加內聚,同時讓業務邏輯更純粹,組件只管發 action 和呈現數據。但不能認爲「redux 不支持異步 action,因此必須加 thunk,必須加 saga」,這是站在抽象的高峯,被概念的雲霧迷了眼。
本來只是三個文件的事,最終擴展成了 redux + react-redux + action type 定義方案 + immutable 數據更新方案 + 異步過程封裝方案。這下好了,要寫一個最簡單的「請求接口而後更新數據」的邏輯,每每須要改動 五、6 個文件。整個邏輯鏈路之長,不只寫起來費勁,看/找起來也費勁。
因而社區給出了整合、封裝甚至重構過的相對完整的類 redux 方案,好比 rematch(Redesigning Redux)、dva。更激進的,有人不肯意受「只能經過 dispatch action 而不是直接調用 reducer 更新 state」的約束,搞出了【action reducer 化】的方案,好比 redux-zero、reduxless。
// redux
dispatch(action) => reducer => newState
// redux-zero
action(state, payload) => newState
複製代碼
圍繞 redux,不只有大量「修補型」方案,還有很多「整合型」、「重構型」和「替代型」方案,正面的說,這是生態繁榮的體現,負面的說,這是對 redux 做爲事實上的【react 狀態管理業界標準】的某種諷刺。這一切究竟是證實 redux 自己不是一個好的設計,仍是說使用者沒用對,把事情搞複雜了?
每個 action,必定對應一個 reducer 處理邏輯,若是 reducer 函數按 action 粒度拆分,每一個 action,對應一個 reducer 函數,而每一個 action 擁有惟一的 type,那麼能夠得出:action、type 和 reducer 是一一對應的。
若是能保證 reducer 不重名,而後 action 和 type 直接複用 reducer 的名稱,那麼 action 就能根據 reducer 自動生成。
// reducer.js
// reducer 的拆分方式有不少
// 這裏每一個 reducer 對應一個 action
// 第一個參數是全局的 state,第二個參數對應 action 中的 payload
export const reducerA = (state, payload) => state
export const reducerB = (state, payload) => state
// action.js
// 指望根據 reducer 自動生成的 actions 對象
export const actions = {
reducerA: payload => ({ type: 'reducerA', payload }),
reducerB: payload => ({ type: 'reducerB', payload })
}
複製代碼
首先,按上述方式拆分的 reducer,須要按以下方式聚合:
// store.js
import initialState from './state.js'
import * as reducers from './reducer.js'
// 聚合 reducer
function reducer(state = initialState, { type, payload }) {
const fn = reducers[type];
return fn ? fn(state, payload) : state;
}
複製代碼
上述寫法要求全部的 reducer 都聚合在 reducer.js 裏,注意,不必定都定義在 reducer.js 裏,能夠分散定義在不一樣文件中,只是在 reducer.js 裏統一導出,這樣就保證了 reducer 不會重名。
有了 reducer 的 map 對象,很容易自動生成 actions 對象:
// action.js
import * as reducers from './reducer.js'
export const actions = Object.keys(reducers).reduce(
(prev, type) => {
prev[type] = payload => ({ type, payload })
return prev
},
{}
)
複製代碼
使用的時候,引入 actions 對象便可:
import { actions } from 'store/action.js'
dispatch(actions.reducerA(payload))
複製代碼
這樣,action 和 type 就都不須要定義了。每次新增邏輯,狀態部分就只須要寫 state 和 reducer 便可。
不過上述方式仍然不夠完美,由於沒有類型。按常規的寫法,action 是有類型定義的,既能夠校驗參數,又有自動補全提示,可丟不得。
// reducer.ts
export interface A {}
export const reducerA = (state: StateType, payload: A) => state
// action.ts
// 常規寫法
import { A } from './reducer.ts'
export const actionA = (payload: A) => ({ type: TYPE, payload })
複製代碼
考慮到 action 函數的參數類型和對應 reducer 第二個參數的類型是一致的,那麼可否既複用 reducer 的名稱,又複用參數類型,在自動生成 actions 對象的同時,連類型也一塊兒自動生成呢?
// 指望生成的 interface
// 關鍵是如何拿到 reducer 函數定義好的 payload 參數的類型,返回值類型其實不須要關心
interface Actions {
reducerA (payload: A): AnyAction
reducerB (payload: B): AnyAction
}
複製代碼
首先不考慮 payload 參數類型,先看如何自動生成 actions 對象的 interface:
可見,經過 keyof 關鍵字,Actions 類型已經拿到了全部的鍵值。
接下來要設置 payload 參數類型,關鍵是如何拿到一個已知的函數類型定義中的第二個參數的類型。TS 裏提供了 infer 關鍵字用於提取類型。
T 表示 args 的類型,是一個數組,T[1] 即第二個參數的類型。
搞定了以上兩個關鍵步驟,剩下的事情就比較簡單了:
這樣咱們就有了類型,每次新增 reducer,都會自動生成最新的 actions 及其類型。
省略了 action 定義,也就省掉了模板代碼中的一大半。其它減省代碼的地方還有:mapDispatchToProps 使用官方推薦的簡寫形式、用 class 定義 state 以直接提取 StateType 類型、封裝 store 定義等,細枝末節很少贅述。
完整案例可參見:codesandbox.io/s/clever-ra…