La transición de Redux a Redux Toolkit (Parte 1)

Introducción de antecedentes

El tiempo inicial es escribir andamios para uso interno, escribir sobre administración de datos e investigar muchas bibliotecas relacionadas con la administración de datos. Como zustand, redux toolkit, unstated-next, etc. Más tarde, estudié cuidadosamente el kit de herramientas de redux y sentí que era demasiado completo, así que eché un vistazo a la implementación del código fuente relevante mientras el hierro estaba caliente. Casi no hay nada nuevo en él, todo se basa en algunos envoltorios redux anteriores, y tengo que darme el visto bueno. Entonces, existe esta serie de artículos sobre la transformación de redux a redux toolkit (el primero y los dos siguientes), a través de una comparación simple combinada con la implementación del código fuente para compartir lo que tiene el kit de herramientas redux (en adelante, rtk). hecho.

propósito de construcción

El propósito de construir rtk es estandarizar la lógica de escribir redux. Originalmente fue creado para resolver los siguientes tres problemas:

  • Construir una tienda redux es demasiado complicado (crear reductores, acciones, actionCreators, etc.)
  • Para que redux sea más útil, los usuarios deben importar varias bibliotecas (como redux-thunk, redux-sagger, etc.)
  • redux requiere una gran cantidad de código repetitivo

Entonces, para simplificar todo el proceso de uso, nació rtx. Al mismo tiempo, rtk también proporciona una herramienta muy útil para obtener y almacenar datos en caché, rtk query. De esta manera, se construye un sistema completo.

Análisis paso a paso

Usemos un caso simple para comparar las diferencias entre el uso de redux y rtk. Aquí, el caso de suma y resta digital simple en el sitio web oficial de rtx (como se muestra en la figura a continuación) se utiliza para el análisis comparativo.imagen

  • Parte comun:

页面结构
<!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();
  • implementación redux

De acuerdo con la lógica que escribimos redux antes, el código simple es el siguiente:

/*
 * @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
}

En el interior, seguimos pensando como de costumbre, definimos actionCreator, definimos reducer para procesar la acción y luego admitimos la impresión del registro introduciendo el registrador e introducimos el thunk para admitir el envío, una función asincrónica. Conozca estas lógicas, por lo que ya no será demasiada descripción.

  • implementación del kit de herramientas redux

En esta parte, usamos rtk para reescribir la lógica anterior. Para facilitar la comparación, la reescribiremos paso a paso.

  1. Cambio de acción y reductor
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(就是下面截图这玩意儿)。

herramienta rtx-dev.png 最后调用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的一些基本函数实现就已经完成了,想要全面了解每个细节,我建议直接去读源码。

写在最后

Se puede ver que no hay nada nuevo en la implementación de rtx, pero su lógica de uso nos trae una gran comodidad. En el próximo artículo, se seguirá estudiando en profundidad algo de lógica asíncrona y consulta rtk, así que estad atentos, gracias.

enlaces relacionados

sitio web oficial del kit de herramientas redux

Supongo que te gusta

Origin juejin.im/post/7116447560163852318
Recomendado
Clasificación