Introdução em segundo plano
O tempo de front-end está escrevendo andaimes para uso interno, escrevendo sobre gerenciamento de dados e pesquisando muitas bibliotecas relacionadas ao gerenciamento de dados. Como zustand, redux toolkit, unstated-next, etc. Mais tarde, estudei cuidadosamente o kit de ferramentas redux e senti que era muito abrangente, então dei uma olhada na implementação do código-fonte relevante enquanto o ferro estava quente. Não há quase nada de novo nele, é tudo baseado em alguns wrappers redux anteriores, e eu tenho que me dar um polegar para cima. Então existe essa série de artigos sobre a transformação do redux para redux toolkit (o primeiro e os próximos dois), através de uma simples comparação combinada com a implementação do código fonte para compartilhar o que o redux toolkit (doravante referido como rtk) tem feito.
construir propósito
O propósito de construir rtk é padronizar a lógica de escrever redux. Ele foi originalmente criado para resolver os três problemas a seguir:
- Construir uma loja redux é muito complicado (criar redutores, ações, actionCreators, etc.)
- Para tornar o redux mais útil, os usuários devem importar várias bibliotecas (como redux-thunk, redux-sagger, etc.)
- redux requer muito código clichê
Assim, para simplificar todo o processo de uso, surgiu o rtx. Ao mesmo tempo, o rtk também fornece uma ferramenta muito útil para obter e armazenar dados em cache, a consulta rtk. Desta forma, um sistema completo é construído.
Análise passo a passo
Vamos usar um caso simples para comparar as diferenças entre redux e rtk em uso. Aqui, o caso de adição e subtração digital simples no site oficial rtx (conforme mostrado na figura abaixo) é usado para análise comparativa.
-
Parte comum:
页面结构
<!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();
-
implementação redux
De acordo com a lógica que escrevemos redux antes, o código simples é o seguinte:
/*
* @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
}
Dentro ainda pensamos como de costume, definimos o actionCreator, definimos o redutor para processar a ação, e então suportamos a impressão do log introduzindo o logger, e introduzimos o thunk para suportar o dispatch, um antiion assíncrono. conheça essas lógicas, então elas não serão mais Demasiadas descrição.
-
implementação do kit de ferramentas redux
Nesta parte, usamos rtk para reescrever a lógica acima, para facilitar a comparação, reescreveremos passo a passo.
- Troca de ação e redutor
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的一些基本函数实现就已经完成了,想要全面了解每个细节,我建议直接去读源码。
写在最后
Pode-se ver que não há nada de novo na implementação do rtx, mas sua lógica de uso nos traz grande comodidade. No próximo artigo, algumas lógicas assíncronas e consultas rtk continuarão sendo estudadas em profundidade, portanto, fique atento, obrigado.