redux vs redux-toolkit 及源碼實(shí)現(xiàn)

前言

為何講這個內(nèi)容?以為后續(xù)大家會使用 redux-toolkit,資產(chǎn)上周做了 redux-toolkit 的升級順便了解了相關(guān)內(nèi)容,產(chǎn)出了這篇文章。

另外補(bǔ)齊一下在 React 數(shù)據(jù)流這個知識板塊的完整性。

在之前的周分享中已經(jīng)分享過了React 中的數(shù)據(jù)流,react-redux 的一些實(shí)現(xiàn),redux 中中間件的實(shí)現(xiàn),以及 Mobx 的使用以及丐版實(shí)現(xiàn)。

對于 Redux 本身尚未涉及,趁著使用 redux-toolkit 的機(jī)會一起了解一下 Redux 的實(shí)現(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 其實(shí)都擁有的,只是他們使用的方式不同,例如 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)進(jìn)行修改,而不需要手動編寫不可變性的邏輯
:::

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

在 Toolkit 中直接使用 createSlice 更加方便,能夠直接導(dǎo)出 reducer 和 action,直接在一個方法中能夠獲取到對應(yīng)內(nèi)容不在需要多處定義。

Redux 源碼實(shí)現(xiàn)

簡單的狀態(tài)管理

所謂的狀態(tài)其實(shí)就是數(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)實(shí)現(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)管理器

上述的實(shí)現(xiàn)能夠更改狀態(tài)和監(jiān)聽狀態(tài)的改變。但是上述改變 state 的方式過于隨便了,我們可以任意修改 state 中的數(shù)據(jù),changeState({ sex: "female" }),即使 sex 不存在于 initialState 中,所以我們需要約束只能夠修改 name/age 屬性

通過一個 plan 函數(shù)來規(guī)定UPDATE_NAMEUPDATE_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。

但是實(shí)際項(xiàng)目中可能存在不同模塊的 state,如果都把 state 的執(zhí)行計劃寫在同一個 reducer 中龐大有復(fù)雜。

因此在常見的項(xiàng)目中會按模塊拆分不同的 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;
  };
};

代碼跑起來?。?/p>

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 樹很龐大,在項(xiàng)目中使用的時候我們都在 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 都會進(jìn)到 default 項(xiàng),返回 initialState

Redux-Toolkit 源碼實(shí)現(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 的實(shí)現(xiàn)是基于 redux 來實(shí)現(xiàn)的,且使用上也大同小異,無破壞性變更。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容