从Redux到Redux Toolkit的转变(上)
背景介绍
前端时间在写内部用的脚手架,写到数据管理这块儿,调研了很多数据管理相关的库。比如zustand、redux toolkit、unstated-next等等。后来仔细的研究了下redux toolkit觉得它实在是太全乎了,于是又趁热打铁看了看相关的源码实现。里面几乎没有任何新的东西,全都是基于之前redux的一些封装,不得不让我竖起了大拇指。于是乎便有了这个关于redux向redux toolkit转变的系列文章(上、下两篇),通过简单的对比结合源码的实现来分享下redux toolkit(以下简称rtk)到底做了些什么。
构建目的
构建rtk的目的就是为了要标准化书写redux的逻辑。它最初创建的目的是为了解决以下三个问题:
- 构建一个redux store过于繁杂(创建reducer,action,actionCreator等等)
- 为了使得redux能变得更有用,使用者这必须得引入各种类库(比如redux-thunk, redux-sagger等等)
- redux需要很多样板代码
所以为了简化整个使用流程,rtx应运而生。同时,rtk也提供非常有用的获取和缓存数据的工具rtk query。这样,一整个完成的体系就构建出来了。
逐步解析
下面我们通过一个简单的案例,来对比下redux和rtk在使用方法上的不同。这里,就采用rtx官网的简单数字加减的案例(如下图所示)来进行对比分析。
-
公用部分:
页面结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
display: flex;
align-items: center;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<button class="minuse">-</button>
<div id="value"></div>
<button class="add">+</button>
<button class="add-async">add async</button>
</div>
</div>
</body>
</html>
公用js
const $value = document.getElementById("value");
function render() {
$value.innerHTML = store.getState()?.num
}
function eventListener() {
const $add = document.querySelector('.add');
const $minuse = document.querySelector('.minuse');
const $addAsync = document.querySelector('.add-async');
$add.addEventListener('click', function() {
store.dispatch(increment(2));
})
$minuse.addEventListener('click', function() {
store.dispatch(decrement(3));
})
$addAsync.addEventListener('click', function() {
store.dispatch((dispatch) => {
setTimeout(() => {
dispatch(increment(3))
}, 1000)
})
})
}
//这里的store是由redux或者redux-toolkit创建出来的store
store.subscribe(render);
render();
eventListener();
-
redux实现
按照之前我们写redux的逻辑,简单的代码如下:
/*
* @Author: ryyyyy
* @Date: 2022-07-03 08:22:26
* @LastEditors: ryyyyy
* @LastEditTime: 2022-07-04 16:31:43
* @FilePath: /toolkit-without-react/src/redux-index.js
* @Description:
*
*/
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
const incrementType = "counter/increment";
const decrementType = "counter/decrement";
const increment = (count=2) => {
return {
type: incrementType,
payload: count
};
};
const decrement = (count=2) => {
return {
type: decrementType,
payload: count
};
};
const counterReducer = (state, action) => {
switch (action.type) {
case incrementType:
return { ...state, num: state.num + (action?.payload || 1) };
case decrementType:
return { ...state, num: state.num - (action.payload || 1) };
default:
return state;
}
};
const store = createStore(counterReducer, {num: 0}, applyMiddleware(logger, thunk));
export default store;
export {
increment,
decrement
}
里面我们还是想往常一样,定义了actionCreator,定义了reducer处理action,然后通过引入了logger支持打印log,引入thunk支持dispatch一个异步的antion,用过redux的同学肯定都知道这些个逻辑,就不再过多描述。
-
redux toolkit实现
这部分,我们用rtk来重写上面的逻辑,为了方便对比,我们一步一步的来做重新改写。
- action和reducer替换
import {createAction,createReducer } from '@reduxjs/toolkit';
const increment = createAction('counter/increment');
const decrement = createAction('counter/decrement');
//写法一
const counterReducer = createReducer({num: 0}, (builder) => {
builder
.addCase(increment, (state, action) => {
state.num += action.payload
})
.addCase(decrement, (state, action) => {
state.num -= action.payload
})
})
//写法二
const counterReducer = createReducer({num: 0}, {
[increment]: (state, action) => {state.num += action.payload},
[decrement]: (state, action) => {state.num -= action.payload},
})
非常简单,这里通过createAction传入type就生成了increment和decrement两个actionCreator,createAction的第二个参数prepareAction?,用于对传入的action进行增强(后面源码分析会讲到)。然后利用createReducer简单的传入initialState和对应的描述各个reducer分支的逻辑,就能直接生成reducer。这里支持Builder Callback和Map Object两种写法,前者可以通过builder链式的调用,配置不同的reducer分支逻辑;后者,则通过map的形式,更为直观的给出各个reducer分支的配置。细心的读者还可以观察到,在各个reducer分支的实现里面,我们是直接操作state,是的,createReducer里面内置了immer的逻辑,简直棒呆! 2. store的生成
const store = configureStore({
reducer: counterReducer,
middleware: [logger]
})
其实这里跟原本的redux的createStore差不太多,只不过这里形参采用map的形式,更让你明白各个字段都是用作什么的。当然,这里不止这么些个参数配置,想要了解详情的小伙伴,请移步rtk官网。细心的读者会发现,这里我们并没有引入redux-thunk,哈哈哈,因为在其内部实现中已经帮我们内置了thunk的功能,突出一个方便。想不想更方便一点呢,当然有办法,rtx提供了一个createSlice的方法,讲上面的action,reducer等都融合在了一起,参看下面的代码:
const counterSlice = createSlice({
name: 'counter', //用作命名空间,和别的slice区分开
initialState: {num: 0},
reducers: {
increment(state, action) {
state.num += action.payload;
},
decrement(state, action) {
state.num -= action.payload;
}
}
})
const {reducer, actions} = counterSlice;
const {increment, decrement} = actions;
通过createSlice返回了actions和reducer,真的不能更简单了。下面,我们参照源码,来实现下rtk上述几个基本方法。
rtx源码实现
- configureStore
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
import isPlainObject from './utils/isPlainObject'; //工具函数,判断是不是一个对象
import thunk from 'redux-thunk';
const configureStore = (options) => {
const {
reducer,
middleware = undefined,
devTools = true,
preloadedState = undefined
} = options;
let rootReducer;
if (typeof reducer === 'function') {
rootReducer = reducer
} else if (isPlainObject(reducer)) {
rootReducer = combineReducers(reducer);
} else {
throw new Error(
'"reducer" is a required argument, and must be a function or an object of functions that can be passed to combineReducers'
)
}
const composedMiddleware = Array.isArray(middleware) ? middleware.concat(thunk) : [thunk];
const composeEnhancers = devTools ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||compose : compose;
return createStore(reducer, preloadedState, composeEnhancers(applyMiddleware(...composedMiddleware)));
}
export default configureStore;
函数接受四个参数(官网是5个,我简化了)。首先reducer的创建,如果是函数的话,表示传入的是单个reducer,如果是对象,则使用combineReducers进行组合。然后是middleware,根据传入的middleware组合内置的thunk构建新的middleware。接着是enhancer部分,这里会判断是否打开Redux DevTools Extension(就是下面截图这玩意儿)。
最后调用redux的createStore,齐活儿。从这里我们就能看出,其实并没有什么新的逻辑,全是redux的一些概念。
- createAction
import isPlainObject from "./utils/isPlainObject";
const createAction = (type, prepareAction) => {
const actionCreator = (...args) => {
if (prepareAction) {
let prepared = prepareAction(...args);
if (!isPlainObject(prepared)) {
throw new Error('prepareAction did not return an object')
}
return {
type,
payload: prepared.payload
}
}
return {
type,
payload: args[0]
}
}
actionCreator.type = type;
actionCreator.toString = () => `${type}`;
return actionCreator;
}
export default createAction;
action的创建比较简单,直接返回了一个actionCreator。因为我们可以通过increment.type或者increment.toString()拿到action的type,所以在actionCreator上挂了两个属性。关于第二个参数prepareAction,如果传入了,则根据它生成新的payload,是对之前的payload的一个增强。
/*
* @Author: ryyyyy
* @Date: 2022-07-04 13:53:49
* @LastEditors: ryyyyy
* @LastEditTime: 2022-07-04 15:04:38
* @FilePath: /toolkit-without-react/toolkit/createReducer.js
* @Description:
*
*/
import produce from "immer";
export const executeReducerBuilderCallback = (builderCallback) => {
const actionsMap = {};
const builder = {
addCase: (typeOrActionCreator, reducer) => {
const type =
typeof typeOrActionCreator === "string"
? typeOrActionCreator
: typeOrActionCreator.type;
if (!actionsMap[type]) actionsMap[type] = reducer;
return builder;
},
};
builderCallback(builder);
return [actionsMap];
};
const createReducer = (initialState, mapOrBuilderCallback) => {
function reducer(state = initialState, action) {
const type = typeof mapOrBuilderCallback;
if (type !== "function" && type !== "object") {
throw new Error(
"mapOrBuilderCallback must be a map or a builder function"
);
}
let [actionsMap] =
type === "function"
? executeReducerBuilderCallback(mapOrBuilderCallback)
: [mapOrBuilderCallback];
let reducer = actionsMap[action.type];
if (reducer) {
return produce(state, (draft) => {
reducer(draft, action);
});
}
return state;
}
return reducer;
};
export default createReducer;
别看createReducer的代码多,因为是为了兼容上面两种写法,所以显得代码多了些。内部返回了一个reducer。由第二个参数mapOrBuilderCallback,来决定如何获取actionsMap。然后根据action的type来确定最后使用reducer的哪个分支actionsMap[action.type]。内部通过immer的produce方法实现了immutable的数据保证。
- createSlice
import createAction from "./createAction";
import createReducer from "./createReducer";
const createSlice = (options) => {
const {name, initialState, reducers} = options;
const actions = {}, newReducers = {};
Object.keys(reducers).forEach((key) => {
const type = `${name}/${key}`;
actions[key] = createAction(type);
newReducers[type] = reducers[key];
})
return {
actions,
reducer: createReducer(initialState, newReducers)
}
}
export default createSlice;
这里内部主要调用了上述createAction和createReducer去生成对应的action和reducer。值得注意的是,传入createReducer的reducer需要重新构建,因为其对应的action是用命名空间加上原来的reducers配置的key生成的新的key。到此,rtk的一些基本函数实现就已经完成了,想要全面了解每个细节,我建议直接去读源码。
写在最后
可以看到,rtx的实现并没有什么新的东西,但是其用法逻辑上确是给我们带来了很大的便利。下一篇文章,会接着一些异步逻辑,以及rtk query继续深入研究,敬请期待,谢谢。
相关链接
转载自:https://juejin.cn/post/7116447560163852318