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 中直接使用 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_NAME
和UPDATE_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
redux vs redux-toolkit 及源码实现的更多相关文章
- Redux学习之解读applyMiddleware源码深入middleware工作机制
随笔前言 在上一周的学习中,我们熟悉了如何通过redux去管理数据,而在这一节中,我们将一起深入到redux的知识中学习. 首先谈一谈为什么要用到middleware 我们知道在一个简单的数据流场景中 ...
- redux 源码浅析
redux 源码浅析 redux 版本号: "redux": "4.0.5" redux 作为一个十分常用的状态容器库, 大家都应该见识过, 他很小巧, 只有 ...
- 微信小程序实例源码大全
微信小程序实例源码大全下载 微信小应用示例代码(phodal/weapp-quick)源码链接:https://github.com/phodal/weapp-quick 微信小应用地图定位demo( ...
- 微信小程序实例源码大全下载
小程序QQ交流群:131894955 小程序开发直播腾讯课堂: https://edu.csdn.net/course/detail/6743 微信小程序实例源码大全下载 微信小应用示例代码(p ...
- 史上最全的 Redux 源码分析
前言 用 React + Redux 已经一段时间了,记得刚开始用Redux 的时候感觉非常绕,总搞不起里面的关系,如果大家用一段时间Redux又看了它的源码话,对你的理解会有很大的帮助.看完后,在回 ...
- 通过Redux源码学习基础概念一:简单例子入门
最近公司有个项目使用react+redux来做前端部分的实现,正好有机会学习一下redux,也和小伙伴们分享一下学习的经验. 首先声明一下,这篇文章讲的是Redux的基本概念和实现,不包括react- ...
- redux:applyMiddleware源码解读
前言: 笔者之前也有一篇关于applyMiddleware的总结.是applyMiddleware的浅析. 现在阅读了一下redux的源码.下面说说我的理解. 概要源码: step 1: apply ...
- Redux源码分析之createStore
接着前面的,我们继续,打开createStore.js, 直接看最后, createStore返回的就是一个带着5个方法的对象. return { dispatch, subscribe, getSt ...
- Redux源码分析之applyMiddleware
Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...
- Redux源码分析之基本概念
Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分 ...
随机推荐
- 中电资讯-乘风破浪数字经济,银行如何Hold数据?
近期各类规划密集发布人行金融科技发展规划发布 金融标准化"十四五"规划发布 "十四五"信息化规划发布 -- 和数据应用有关的各项政策密集出炉 数字经济发展中如何 ...
- Python 潮流周刊#82:美国 CIA 如何使用 Python?(摘要)
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...
- docker-entrypoint.sh 文件的用处
参考出处很多著名库的 Dockerfile 文件中,通常都是 ENTRYPOINT 字段会是这样: ENTRYPOINT ["docker-entrypoint.sh"]这里我们参 ...
- Unity TheHeretic Gawain Demo 异教徒Demo技术学习
<异教徒 Heretic>是Unity在2019年GDC大会上展示的一款技术Demo,部分资源于2020年中旬公开下载. 这款Demo主要用于展示Unity在数字人技术领域的最新进展,尤其 ...
- Solution -「AGC 020F」Arcs on a Circle
\(\mathscr{Description}\) Link. 在一个周长为 \(c\) 的圆周上放置长度分别为 \(l_1,l_2,\cdots,l_n\) 的弧,每条弧的位置独立均匀随机. ...
- C# 获取系统声卡音频数据,并绘制波形
//by wgscd //date:2022/11/7 UI: <Path Stroke="Red" Data="{Binding path}" Rend ...
- winform 引用AForge调用摄像头拍照
Nuget安装这个2个: AForge.Controls; AForge.Video.DirectShow; code: namespace WindowsFormsApp1 { partial cl ...
- SpringCloud Alibaba(四) - Nacos 配置中心
1.环境搭建 1.1 依赖 <!-- nacos注册中心 注解 @EnableDiscoveryClient --> <dependency> <groupId>c ...
- 一级缓存和二级缓存--mybatis|hibernate
一级缓存和二级缓存的区别: 主要的不同是它们的作用范围不同. 一级缓存是session级别的. 也就是只有在同一个session里缓存才起作用,当这个session关闭后这个缓存就不存在了. 而二级缓 ...
- ArrayBlockingQueue源码剖析
生产者-消费者ArrayBlockingQueue是一个实现了BlockingQueue接口的类,其可以很方便的实现生产者-消费者模式.用法如下: class Producer implements ...