動態

詳情 返回 返回

redux vs redux-toolkit 及源碼實現 - 動態 詳情

我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。

本文作者:霜序

前言

為何講這個內容?以為後續大家會使用 redux-toolkit,資產上週做了 redux-toolkit 的升級順便了解了相關內容,產出了這篇文章。

另外補齊一下在 React 數據流這個知識板塊的完整性。

  • React 中的數據流管理
  • 認識一下 Mobx

在之前的周分享中已經分享過了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 的函數
💡 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

  • 大數據分佈式任務調度系統——Taier
  • 輕量級的 Web IDE UI 框架——Molecule
  • 針對大數據領域的 SQL Parser 項目——dt-sql-parser
  • 袋鼠雲數棧前端團隊代碼評審工程實踐文檔——code-review-practices
  • 一個速度更快、配置更靈活、使用更簡單的模塊打包器——ko
  • 一個針對 antd 的組件測試工具庫——ant-design-testing
user avatar tianmiaogongzuoshi_5ca47d59bef41 頭像 alibabawenyujishu 頭像 smalike 頭像 freeman_tian 頭像 aqiongbei 頭像 longlong688 頭像 inslog 頭像 banana_god 頭像 zero_dev 頭像 yuzhihui 頭像 ccVue 頭像 wmbuke 頭像
點贊 109 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.