从Redux到Redux Toolkit的转变(上)

1,726 阅读6分钟

背景介绍

前端时间在写内部用的脚手架,写到数据管理这块儿,调研了很多数据管理相关的库。比如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官网的简单数字加减的案例(如下图所示)来进行对比分析。 image

  • 公用部分:

页面结构
<!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来重写上面的逻辑,为了方便对比,我们一步一步的来做重新改写。

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

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

写在最后

可以看到,rtx的实现并没有什么新的东西,但是其用法逻辑上确是给我们带来了很大的便利。下一篇文章,会接着一些异步逻辑,以及rtk query继续深入研究,敬请期待,谢谢。

相关链接

redux toolkit官网