redux vs redux-toolkit 及源码实现

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:霜序

前言

为何讲这个内容?以为后续大家会使用 redux-toolkit,资产上周做了 redux-toolkit 的升级顺便了解了相关内容,产出了这篇文章。

另外补齐一下在 React 数据流这个知识板块的完整性。

在之前的周分享中已经分享过了React 中的数据流,react-redux 的一些实现,redux 中中间件的实现,以及 Mobx 的使用以及丐版实现。

对于 Redux 本身尚未涉及,趁着使用 redux-toolkit 的机会一起了解一下 Redux 的实现。

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

创建 store,代码内部还是调用的 Redux 的 createStore 方法

const store = configureStore({     reducer: {         counter: counterReducer,         user: userReducer,     }, }); 

createAction + createReducer

  • createAction
    创建 Redux 中的 action 创建函数
function createAction(type, prepareAction?) 

redux 中 action 的创建以及使用

const updateName = (name: string) => ({ type: "user/UPDATE_NAME", name }); const updateAge = (age: number) => ({ type: "user/UPDATE_AGE", age }); 

Toolkit 中 action 的创建以及使用

// 第一种 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
    创建 Redux reducer 的函数

:::info
💡 createReducer 使用 Immer 库,可以在 reducer 中直接对状态进行修改,而不需要手动编写不可变性的逻辑  
:::

Redux 中 reducer 的创建

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 的创建

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

接受初始状态、reducer 函数对象和 slice name 的函数,并自动生成与 reducer 和 state 对应的动作创建者和动作类型

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 创建一个分片,每一个分片代表某一个业务的数据状态处理。在其中可以完成 action 和 reducer 的创建。

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 slice demo

在 Toolkit 中直接使用 createSlice 更加方便,能够直接导出 reducer 和 action,直接在一个方法中能够获取到对应内容不在需要多处定义。

Redux 源码实现

简单的状态管理

所谓的状态其实就是数据,例如用户中的 name

let state = {   name: "shuangxu" }  // 使用状态 console.log(state.name)  // 更改状态 state.name = "FBB" 

上述代码中存在问题,当我们修改了状态之后无法通知到使用状态的函数,需要引入发布订阅模式来解决这个问题

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"); 

在上述代码中,我们已经实现了更改变量能够通知到对应的监听函数。但是上述代码并不通用,需要将公共方法封装起来。

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" }); 

约束状态管理器

上述的实现能够更改状态和监听状态的改变。但是上述改变 state 的方式过于随便了,我们可以任意修改 state 中的数据,changeState({ sex: "female" }),即使 sex 不存在于 initialState 中,所以我们需要约束只能够修改 name/age 属性

通过一个 plan 函数来规定UPDATE_NAMEUPDATE_AGE方式更新对应属性

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 函数

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 的执行计划写在同一个 reducer 中庞大有复杂。

因此在常见的项目中会按模块拆分不同的 reducer,最后在一个函数中将 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 函数   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

在上一节的代码中,我们 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 函数,可以自动获取到每一个 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() })代码能够实现如下效果:

  • createStore 的时候,一个不匹配任何 type 的 action,来触发state = reducer(state, action)
  • 每个 reducer 都会进到 default 项,返回 initialState

Redux-Toolkit 源码实现

configureStore

接受一个含有 reducer 的对象作为参数,内部调用 redux 的 createStore 创建出 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 返回的是一个函数,接受第一个参数 type 返回{ type: 'user/UPDATE_NAME', payload: undefined };对于具体的 payload 值需要传入第二个参数来改变

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 都是一个函数(state = initialState, action) => {},因此 createReducer 返回值为函数

通过一个 createReducer 函数,内部还需要知道每一个 action 对应的操作

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 的第二个参数,构建出 action 对应的操作方法 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 }三个参数。通过 reducers 中相关参数得到对应的 actions 和 reducer。

在 createSlice 中主要还是靠 createAction 和 createReducer 方法。通过 name 和 reducers 的每一个属性拼接成为 action.type,调用 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);     },   }; } 

总结

在本文讲解了 Redux-Toolkit 基础使用,从 redux 的源码出发解析了 redux-toolkit 的源码,从源码中也能够看出来 toolkit 的实现是基于 redux 来实现的,且使用上也大同小异,无破坏性变更。

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

发表评论

评论已关闭。

相关文章