redux vs redux-toolkit 及源碼實現(xiàn)
我們是袋鼠云數(shù)棧 UED 團隊,致力于打造優(yōu)秀的一站式數(shù)據(jù)中臺產(chǎn)品。我們始終保持工匠精神,探索前端道路,為社區(qū)積累并傳播經(jīng)驗價值。
本文作者:霜序
前言
為何講這個內(nèi)容?以為后續(xù)大家會使用 redux-toolkit,資產(chǎn)上周做了 redux-toolkit 的升級順便了解了相關(guān)內(nèi)容,產(chǎn)出了這篇文章。
另外補齊一下在 React 數(shù)據(jù)流這個知識板塊的完整性。
在之前的周分享中已經(jīng)分享過了React 中的數(shù)據(jù)流,react-redux 的一些實現(xiàn),redux 中中間件的實現(xiàn),以及 Mobx 的使用以及丐版實現(xiàn)。
對于 Redux 本身尚未涉及,趁著使用 redux-toolkit 的機會一起了解一下 Redux 的實現(xiàn)。
Redux-Toolkit
Redux-Toolkit 是 基于 Redux 的二次封裝,開箱即用的 Redux 工具,比 Redux 更加簡單方便。
?? Why to use Redux-Toolkit?
- "Configuring a Redux store is too complicated"
- "I have to add a lot of packages to get Redux to do anything useful"
- "Redux requires too much boilerplate code"
Toolkit 使用
Redux 該有的概念,Toolkit 其實都擁有的,只是他們使用的方式不同,例如 reducer / actions 等等,在 Toolkit 中都是隨處可見的。
configureStore
創(chuàng)建 store,代碼內(nèi)部還是調(diào)用的 Redux 的 createStore 方法
const store = configureStore({
reducer: {
counter: counterReducer,
user: userReducer,
},
});
createAction + createReducer
- createAction
創(chuàng)建 Redux 中的 action 創(chuàng)建函數(shù)
function createAction(type, prepareAction?)
redux 中 action 的創(chuàng)建以及使用
const updateName = (name: string) => ({ type: "user/UPDATE_NAME", name });
const updateAge = (age: number) => ({ type: "user/UPDATE_AGE", age });
Toolkit 中 action 的創(chuàng)建以及使用
// 第一種
const updateName = createAction<{ name: string }>("user/UPDATE_NAME");
const updateAge = createAction<{ age: number }>("user/UPDATE_AGE");
updateName(); // { type: 'user/UPDATE_NAME', payload: undefined }
updateName({ name: "FBB" }); // { type: 'user/UPDATE_NAME', payload: { name: 'FBB' } }
updateAge({ age: 18 });
// 第二種
const updateName = createAction("user/UPDATE_NAME", (name: string) => ({
payload: {
name,
},
}));
const updateAge = createAction("user/UPDATE_AGE", (age: number) => ({
payload: {
age,
},
}));
updateName("FBB");
updateAge(18);
- createReducer
創(chuàng)建 Redux reducer 的函數(shù)
:::info
?? createReducer 使用 Immer 庫,可以在 reducer 中直接對狀態(tài)進行修改,而不需要手動編寫不可變性的邏輯
:::
Redux 中 reducer 的創(chuàng)建
export const userReducer = (
state = initialUserState,
action: { type: string; [propName: string]: any }
) => {
switch (action.type) {
case "user/UPDATE_NAME":
return { ...state, name: action.name };
case "user/UPDATE_AGE":
return { ...state, age: action.age };
default:
return state;
}
};
Toolkit 中 reducer 的創(chuàng)建
export const userReducer = createReducer(initialUserState, (builder) => {
builder
.addCase(updateAge, (state, action) => {
state.age = action.payload.age;
})
.addCase(updateName, (state, action) => {
state.name = action.payload.name;
});
});
toolkit 提供的 createAction 和 createReducer 能夠幫我們簡化 Redux 中一些模版語法,但是整體的使用還是差不多的,我們依舊需要 action 文件和 reducer 文件,做了改善但是不多。
redux demo toolkit createReducer demo
createSlice
接受初始狀態(tài)、reducer 函數(shù)對象和 slice name 的函數(shù),并自動生成與 reducer 和 state 對應(yīng)的動作創(chuàng)建者和動作類型
const userSlice = createSlice({
name: "user",
initialState: {
age: 22,
name: "shuangxu",
},
reducers: {
updateName: (state, action: PayloadAction<string>) => {
state.name = action.payload;
},
updateAge: (state, action: PayloadAction<number>) => {
state.age = action.payload;
},
},
})
使用 createSlice 創(chuàng)建一個分片,每一個分片代表某一個業(yè)務(wù)的數(shù)據(jù)狀態(tài)處理。在其中可以完成 action 和 reducer 的創(chuàng)建。
export const userSliceName = userSlice.name;
export const { updateAge, updateName } = userSlice.actions;
export const userReducer = userSlice.reducer;
const store = configureStore({
reducer: {
[counterSliceName]: counterReducer,
[userSliceName]: userReducer,
},
});
在 Toolkit 中直接使用 createSlice 更加方便,能夠直接導(dǎo)出 reducer 和 action,直接在一個方法中能夠獲取到對應(yīng)內(nèi)容不在需要多處定義。
Redux 源碼實現(xiàn)
簡單的狀態(tài)管理
所謂的狀態(tài)其實就是數(shù)據(jù),例如用戶中的 name
let state = {
name: "shuangxu"
}
// 使用狀態(tài)
console.log(state.name)
// 更改狀態(tài)
state.name = "FBB"
上述代碼中存在問題,當(dāng)我們修改了狀態(tài)之后無法通知到使用狀態(tài)的函數(shù),需要引入發(fā)布訂閱模式來解決這個問題
const state = {
name: "shuangxu",
};
const listeners = [];
const subscribe = (listener) => {
listeners.push(listener);
};
const changeName = (name) => {
state.name = name;
listeners.forEach((listener) => {
listener?.();
});
};
subscribe(() => console.log(state.name));
changeName("FBB");
changeName("LuckyFBB");
在上述代碼中,我們已經(jīng)實現(xiàn)了更改變量能夠通知到對應(yīng)的監(jiān)聽函數(shù)。但是上述代碼并不通用,需要將公共方法封裝起來。
const createStore = (initialState) => {
let state = initialState;
let listeners = [];
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter((fn) => fn !== listener);
};
};
const changeState = (newState) => {
state = { ...state, ...newState };
listeners.forEach((listener) => {
listener?.();
});
};
const getState = () => state;
return {
subscribe,
changeState,
getState,
};
};
// example
const { getState, changeState, subscribe } = createStore({
name: "shuangxu",
age: 19,
});
subscribe(() => console.log(getState().name, getState().age));
changeState({ name: "FBB" }); // FBB 19
changeState({ age: 26 }); // FBB 26
changeState({ sex: "female" });
約束狀態(tài)管理器
上述的實現(xiàn)能夠更改狀態(tài)和監(jiān)聽狀態(tài)的改變。但是上述改變 state 的方式過于隨便了,我們可以任意修改 state 中的數(shù)據(jù),changeState({ sex: "female" }),即使 sex 不存在于 initialState 中,所以我們需要約束只能夠修改 name/age 屬性
通過一個 plan 函數(shù)來規(guī)定UPDATE_NAME和UPDATE_AGE方式更新對應(yīng)屬性
const plan = (state, action) => {
switch (action.type) {
case "UPDATE_NAME":
return {
...state,
name: action.name,
};
case "UPDATE_AGE":
return {
...state,
age: action.age,
};
default:
return state;
}
};
更改一下 createStore 函數(shù)
const createStore = (plan, initialState) => {
let state = initialState;
let listeners = [];
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter((fn) => fn !== listener);
};
};
const changeState = (action) => {
state = plan(state, action);
listeners.forEach((listener) => {
listener?.();
});
};
const getState = () => state;
return {
subscribe,
changeState,
getState,
};
};
const { getState, changeState, subscribe } = createStore(plan, {
name: "shuangxu",
age: 19,
});
subscribe(() => console.log(getState().name, getState().age));
changeState({ type: "UPDATE_NAME", name: "FBB" });
changeState({ type: "UPDATE_AGE", age: "28" });
changeState({ type: "UPDATE_SEX", sex: "female" });
代碼中的 plan 就是 redux 中的 reducer,changeState 就是 dispatch。
拆分 reducer
reducer 做的事情比較簡單,接收 oldState,通過 action 更新 state。
但是實際項目中可能存在不同模塊的 state,如果都把 state 的執(zhí)行計劃寫在同一個 reducer 中龐大有復(fù)雜。
因此在常見的項目中會按模塊拆分不同的 reducer,最后在一個函數(shù)中將 reducer 合并起來。
const initialState = {
user: { name: "shuangxu", age: 19 },
counter: { count: 1 },
};
// 對于上述 state 我們將其拆分為兩個 reducer
const userReducer = (state, action) => {
switch (action.type) {
case "UPDATE_NAME":
return {
...state,
name: action.name,
};
case "UPDATE_AGE":
return {
...state,
age: action.age,
};
default:
return state;
}
};
const counterReducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return {
count: state.count + 1,
};
case "DECREMENT":
return {
...state,
count: state.count - 1,
};
default:
return state;
}
};
// 整合 reducer
const combineReducers = (reducers) => {
// 返回新的 reducer 函數(shù)
return (state = {}, action) => {
const newState = {};
for (const key in reducers) {
const reducer = reducers[key];
const preStateForKey = state[key];
const nextStateForKey = reducer(preStateForKey, action);
newState[key] = nextStateForKey;
}
return newState;
};
};
代碼跑起來!!
const reducers = combineReducers({
counter: counterReducer,
user: userReducer,
});
const store = createStore(reducers, initialState);
store.subscribe(() => {
const state = store.getState();
console.log(state.counter.count, state.user.name, state.user.age);
});
store.dispatch({ type: "UPDATE_NAME", name: "FBB" }); // 1 FBB 19
store.dispatch({ type: "UPDATE_AGE", age: "28" }); // 1 FBB 28
store.dispatch({ type: "INCREMENT" }); // 2 FBB 28
store.dispatch({ type: "DECREMENT" }); // 1 FBB 28
拆分 state
在上一節(jié)的代碼中,我們 state 還是定義在一起的,會造成 state 樹很龐大,在項目中使用的時候我們都在 reducer 中定義好 initialState 的。
在使用 createStore 的時候,我們可以不傳入 initialState,直接使用store = createStore(reducers)。因此我們要對這種情況作處理。
拆分 state 和 reducer 寫在一起。
const initialUserState = { name: "shuangxu", age: 19 };
const userReducer = (state = initialUserState, action) => {
switch (action.type) {
case "UPDATE_NAME":
return {
...state,
name: action.name,
};
case "UPDATE_AGE":
return {
...state,
age: action.age,
};
default:
return state;
}
};
const initialCounterState = { count: 1 };
const counterReducer = (state = initialCounterState, action) => {
switch (action.type) {
case "INCREMENT":
return {
count: state.count + 1,
};
case "DECREMENT":
return {
...state,
count: state.count - 1,
};
default:
return state;
}
};
更改 createStore 函數(shù),可以自動獲取到每一個 reducer 的 initialState
const createStore = (reducer, initialState = {}) => {
let state = initialState;
let listeners = [];
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter((fn) => fn !== listener);
};
};
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => {
listener?.();
});
};
const getState = () => state;
// 僅僅用于獲取初始值
dispatch({ type: Symbol() });
return {
subscribe,
dispatch,
getState,
};
};
dispatch({ type: Symbol() })代碼能夠?qū)崿F(xiàn)如下效果:
- createStore 的時候,一個不匹配任何 type 的 action,來觸發(fā)
state = reducer(state, action) - 每個 reducer 都會進到 default 項,返回 initialState
Redux-Toolkit 源碼實現(xiàn)
configureStore
接受一個含有 reducer 的對象作為參數(shù),內(nèi)部調(diào)用 redux 的 createStore 創(chuàng)建出 store
import { combineReducers, createStore } from "redux";
export function configureStore({ reducer }: any) {
const rootReducer = combineReducers(reducer);
const store = createStore(rootReducer);
return store;
}
createAction
const updateName = createAction<string>("user/UPDATE_NAME");
const updateName = createAction("user/UPDATE_NAME", (name: string) => ({
payload: {
name,
},
}));
updateName("FBB");
通過上面的示例,能夠分析出來 createAction 返回的是一個函數(shù),接受第一個參數(shù) type 返回{ type: 'user/UPDATE_NAME', payload: undefined };對于具體的 payload 值需要傳入第二個參數(shù)來改變
export const createAction = (type: string, preAction?: Function) => {
function actionCreator(...args: any[]) {
if (!preAction)
return {
type,
payload: args[0],
};
const prepared = preAction(...args);
if (!prepared) {
throw new Error("prepareAction did not return an object");
}
return {
type,
payload: prepared.payload,
};
}
actionCreator.type = type;
return actionCreator;
};
createReducer
export const userReducer = createReducer(initialUserState, (builder) => {
builder
.addCase(updateAge, (state, action) => {
state.age = action.payload.age;
})
.addCase(updateName, (state, action) => {
state.name = action.payload.name;
});
});
每一個 reducer 都是一個函數(shù)(state = initialState, action) => {},因此 createReducer 返回值為函數(shù)
通過一個 createReducer 函數(shù),內(nèi)部還需要知道每一個 action 對應(yīng)的操作
import { produce as createNextState } from "immer";
export const createReducer = (
initialState: any,
builderCallback: (builder: any) => void
) => {
const actionsMap = executeReducerBuilderCallback(builderCallback);
return function reducer(state = initialState, action: any) {
const caseReducer = actionsMap[action.type];
if (!caseReducer) return state;
return createNextState(state, (draft: any) =>
caseReducer(draft, action)
);
};
};
// 通過 createReducer 的第二個參數(shù),構(gòu)建出 action 對應(yīng)的操作方法
export const executeReducerBuilderCallback = (
builderCallback: (builder: any) => void
) => {
const actionsMap: any = {};
const builder = {
addCase(typeOrActionCreator: any, reducer: any) {
const type =
typeof typeOrActionCreator === "string"
? typeOrActionCreator
: typeOrActionCreator.type;
actionsMap[type] = reducer;
return builder;
},
};
builderCallback(builder);
return actionsMap;
};
createSlice
const counterSlice = createSlice({
name: "counter",
initialState: {
count: 1,
},
reducers: {
increment: (state: any) => {
state.count += 1;
},
decrement: (state: any) => {
state.count -= 1;
},
},
});
const counterSliceName = counterSlice.name;
const { increment, decrement } = counterSlice.actions;
const counterReducer = counterSlice.reducer;
createSlice 返回的是一個對象{ name, actions, reducer },接受{ name, initialState, reducers }三個參數(shù)。通過 reducers 中相關(guān)參數(shù)得到對應(yīng)的 actions 和 reducer。
在 createSlice 中主要還是靠 createAction 和 createReducer 方法。通過 name 和 reducers 的每一個屬性拼接成為 action.type,調(diào)用 createReducer 遍歷 reducers 的屬性添加 case
import { createAction } from "./createAction";
import { createReducer } from "./createReducer";
export default function createSlice({ name, initialState, reducers }: any) {
const reducerNames = Object.keys(reducers);
const actionCreators: any = {};
const sliceCaseReducersByType: any = {};
reducerNames.forEach((reducerName) => {
const type = `${name}/${reducerName}`;
const reducerWithPrepare = reducers[reducerName];
actionCreators[reducerName] = createAction(type);
sliceCaseReducersByType[type] = reducerWithPrepare;
});
function buildReducer() {
return createReducer(initialState, (builder) => {
for (let key in sliceCaseReducersByType) {
builder.addCase(key, sliceCaseReducersByType[key]);
}
});
}
return {
name,
actions: actionCreators,
reducer: (state: any, action: any) => {
const _reducer = buildReducer();
return _reducer(state, action);
},
};
}
總結(jié)
在本文講解了 Redux-Toolkit 基礎(chǔ)使用,從 redux 的源碼出發(fā)解析了 redux-toolkit 的源碼,從源碼中也能夠看出來 toolkit 的實現(xiàn)是基于 redux 來實現(xiàn)的,且使用上也大同小異,無破壞性變更。
最后
歡迎關(guān)注【袋鼠云數(shù)棧UED團隊】~
袋鼠云數(shù)棧 UED 團隊持續(xù)為廣大開發(fā)者分享技術(shù)成果,相繼參與開源了歡迎 star

浙公網(wǎng)安備 33010602011771號