Présentation du contexte
Le temps initial consiste à écrire des échafaudages à usage interne, à écrire sur la gestion des données et à rechercher de nombreuses bibliothèques liées à la gestion des données. Tels que zustand, redux toolkit, unstated-next, etc. Plus tard, j'ai étudié attentivement la boîte à outils redux et j'ai estimé qu'elle était trop complète, alors j'ai jeté un coup d'œil à l'implémentation du code source pertinent pendant que le fer était chaud. Il n'y a pratiquement rien de nouveau dedans, tout est basé sur des wrappers redux précédents, et je dois me féliciter. Il y a donc cette série d'articles sur la transformation de redux en redux toolkit (le premier et les deux suivants), à travers une simple comparaison combinée à l'implémentation du code source pour partager ce que le redux toolkit (ci-après dénommé rtk) a Fini.
objectif de construction
Le but de la construction de rtk est de normaliser la logique d'écriture de redux. Il a été initialement créé pour résoudre les trois problèmes suivants :
- Construire un magasin redux est trop compliqué (créer des reducers, des actions, des actionCreators, etc.)
- Afin de rendre redux plus utile, les utilisateurs doivent importer diverses bibliothèques (telles que redux-thunk, redux-sagger, etc.)
- redux nécessite beaucoup de code passe-partout
Ainsi, afin de simplifier l'ensemble du processus d'utilisation, rtx a vu le jour. Dans le même temps, rtk fournit également un outil très utile pour obtenir et mettre en cache des données, la requête rtk. De cette façon, un système complet est construit.
Analyse pas à pas
Utilisons un cas simple pour comparer les différences d'utilisation entre redux et rtk. Ici, le cas de l'addition et de la soustraction numériques simples sur le site officiel de rtx (comme indiqué dans la figure ci-dessous) est utilisé pour une analyse comparative.
-
Partie commune :
页面结构
<!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();
-
implémentation redux
Selon la logique que nous avons écrite redux auparavant, le code simple est le suivant :
/*
* @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
}
À l'intérieur, nous pensons toujours comme d'habitude, définissons l'actionCreator, définissons le réducteur pour traiter l'action, puis prenons en charge l'impression du journal en introduisant le logger, et introduisons le thunk pour prendre en charge l'expédition, un antiion asynchrone. connaître ces logiques, pour qu'elles ne soient plus trop descriptives.
-
implémentation de la boîte à outils redux
Dans cette partie, nous utilisons rtk pour réécrire la logique ci-dessus. Pour faciliter la comparaison, nous allons la réécrire étape par étape.
- Action et remplacement du réducteur
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的一些基本函数实现就已经完成了,想要全面了解每个细节,我建议直接去读源码。
写在最后
On voit qu'il n'y a rien de nouveau dans l'implémentation de rtx, mais sa logique d'utilisation nous apporte un grand confort. Dans le prochain article, certaines logiques asynchrones et requêtes rtk continueront d'être étudiées en profondeur, alors restez à l'écoute, merci.