我们是袋鼠云数栈 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

redux vs redux-toolkit 及源码实现的更多相关文章

  1. Redux学习之解读applyMiddleware源码深入middleware工作机制

    随笔前言 在上一周的学习中,我们熟悉了如何通过redux去管理数据,而在这一节中,我们将一起深入到redux的知识中学习. 首先谈一谈为什么要用到middleware 我们知道在一个简单的数据流场景中 ...

  2. redux 源码浅析

    redux 源码浅析 redux 版本号: "redux": "4.0.5" redux 作为一个十分常用的状态容器库, 大家都应该见识过, 他很小巧, 只有 ...

  3. 微信小程序实例源码大全

    微信小程序实例源码大全下载 微信小应用示例代码(phodal/weapp-quick)源码链接:https://github.com/phodal/weapp-quick 微信小应用地图定位demo( ...

  4. 微信小程序实例源码大全下载

     小程序QQ交流群:131894955  小程序开发直播腾讯课堂:  https://edu.csdn.net/course/detail/6743 微信小程序实例源码大全下载 微信小应用示例代码(p ...

  5. 史上最全的 Redux 源码分析

    前言 用 React + Redux 已经一段时间了,记得刚开始用Redux 的时候感觉非常绕,总搞不起里面的关系,如果大家用一段时间Redux又看了它的源码话,对你的理解会有很大的帮助.看完后,在回 ...

  6. 通过Redux源码学习基础概念一:简单例子入门

    最近公司有个项目使用react+redux来做前端部分的实现,正好有机会学习一下redux,也和小伙伴们分享一下学习的经验. 首先声明一下,这篇文章讲的是Redux的基本概念和实现,不包括react- ...

  7. redux:applyMiddleware源码解读

    前言: 笔者之前也有一篇关于applyMiddleware的总结.是applyMiddleware的浅析. 现在阅读了一下redux的源码.下面说说我的理解. 概要源码: step 1:  apply ...

  8. Redux源码分析之createStore

    接着前面的,我们继续,打开createStore.js, 直接看最后, createStore返回的就是一个带着5个方法的对象. return { dispatch, subscribe, getSt ...

  9. Redux源码分析之applyMiddleware

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

  10. Redux源码分析之基本概念

    Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...

随机推荐

  1. web应用模式、API接口、接口测试工具postman、restful规范

    目录 一.web 应用模式 二.API接口 API概念 三.接口测试工具postman postman介绍 postman下载与使用 四.如何在浏览器中测试 五.restful规范(重要,不难) 概念 ...

  2. IDE提交Git出现husky>pre-commit错误

    若使用IDE提交Git出现以下错误: 则是ES6在提交校验过程中出现了问题,强制提交方式为: 命令行: git commit --no-verify IDEA: 在提交时取消勾选Run Git hoo ...

  3. .NET Core 类型系统(Types System)底层原理浅谈

    C#类型系统 C# 是一种强类型语言. 每个变量和常量都有一个类型,每个求值的表达式也是如此. 每个方法声明都为每个输入参数和返回值指定名称.类型和种类(值.引用或输出). .NET 类库定义了内置数 ...

  4. Unity 3D使用C#脚本实例

    界面 结构 代码 1 using System.Collections; 2 using System.Collections.Generic; 3 using UnityEngine; 4 usin ...

  5. IDEA导入他人的项目时提示“project sdk is not defined”的解决办法

    IDEA导入他人的项目时提示"project sdk is not defined"的解决办法 1.在IDEA中,在有问题的项目上单击鼠标右键,然后选择"Open Mod ...

  6. 《Spring Boot+Vue全栈开发实战-王松2018》一书pdf+源码下载

    下载地址为: 链接:https://pan.baidu.com/s/18lnF2KemQTqkKaCRmMbvXA 提取码:1pie 版权声明:本书版权属于出版社和作者.仅学习使用,请于下载后24小时 ...

  7. 实时音视频入门学习:开源工程WebRTC的技术原理和使用浅析

    本文由ELab技术团队分享,原题"浅谈WebRTC技术原理与应用",有修订和改动. 1.基本介绍 WebRTC(全称 Web Real-Time Communication),即网 ...

  8. 查看Android是否开机启动进入桌面

    adb 或者 串口终端 getprop sys.boot_completed 返回空代表没有进入桌面返回1代表已进入桌面

  9. Solution -「NEERC 2016」Delight for a Cat 的一个尝试

    \(\mathscr{Description}\)   Link.   给定 \(n,k,m_s,m_e\) 和两个长为 \(n\) 的序列 \(\{s\},\{e\}\), 选择一个 \(S\sub ...

  10. .NET 开发的分流抢票软件,不做广告、不收集隐私

    前言 每年春节大家必备的抢票工具Bypass-分流抢票.分流抢票是一款免费无广适用于PC端的自动分流抢票软件. 分流抢票,是以用户为中心.人性化的抢票软件.不做广告.不做推广.不携带病毒.不收集隐私信 ...