A transição do Redux para o Redux Toolkit (Parte 1)

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.imagem

  • 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.

  1. 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(就是下面截图这玩意儿)。

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

写在最后

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.

Links Relacionados

site oficial do kit de ferramentas redux

Acho que você gosta

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