Rematch

876 阅读14分钟

前言

最近在搞一个新项目,涉及到状态管理工具的选型,组内同学有了不同的意见,我建议使用redux,但是组内更多的同学建议使用rematch,原因只有一个:简单

一、Rematch是什么

Redux是一个出色的状态管理工具,有健全的中间件生态与出色的开发工具。

RematchRedux的基础上构建并减少了样板代码和执行了一些最佳实践。

相对与ReduxRematch移除了以下内容:

  • 声明 action 类型
  • action 创建函数
  • thunks
  • store配置
  • mapDispatchToProps
  • sagas

二、为什么是Rematch

中后台切换到react后都在用 Redux 做状态管理,Redux 本身非常轻量,区区上百行代码就实现了一个极简的状态管理框架。我们在源码的阅读过程中可以看到了很多巧妙的设计:高阶组件、洋葱模型、函数式编程。Redux在业界的普及度也极高。为了让Redux更易使用,JS社区里也衍生出了很多基于Redux封装的框架,例如MirrorDvaRematch等等。

ReduxRematchMirrorDva
规避样板代码×
全局Dispatch×××
Action Creator××
异步thunkasync/awaitasync/awaitsaga
可读性
压缩后的包大小-5.1k33.8k22.5k

Rematch的作者吸收融合了DvaMirror的优点和设计思路,设计出了更简单易用的Redux封装框架。

三、Redux框架存在的问题

Redux对于初学者来说简直就是噩梦,他仿佛不是一个状态管理工具,而是一个涉及了众多概念的状态管理模型。要想搞明白Redux如何使用,就要先了解10个以上名词的含义;这还只是Redux的主流程使用中涉及到的名词。Redux的主流程里充斥了各种各样的概念,比如,DispatchReducerCreateStoreApplyMiddlewareComposeCombineReducersActionActionCreatorAction TypeAction PayloadBindActionCreators...

image.png 其实,对于一个使用Redux有经验的开发者来说,Redux的工作流程无非就是StoreDispatchActionReducer,那能不能就只暴露这些Api,而不让开发者感受那么多设计细节呢?

Rematch将这些概念进行了整合,提出了一个更简洁的状态管理模型;这个模型只涉及4个基本概念:ModelDispatchReducer ActionEffect Action

概念解释
ModelRematch的数据层,每一个Model文件对应着一块数据层的数据块
Dispatch全局的对象,用于发送Reducer Action和Effect Action
Reducer Action直接更新数据层的Action
Effect Action用于处理异步任务的Action

样板代码太多

明明只是修改了一个State,我们却至少需要修改组件、Action TypesReducersAction Creators几个文件;明明是强相关的代码却散落得到处都是,我们耗费了大量的时间来书写近乎相同的代码,有时候找个对应的action.type要翻遍几个文件,这还是在没有使用到Redux Saga的情况下。如果用到了Redux Saga,你可能还需要更改Watcher.jstakeEvery/takeLatest监听Action的文件)、Worker.js(汇总每一类sagas的文件)。有的时候,明明思路很清晰地在编写代码,可是在一连串改完这几个文件后,思路常常会被打断.

针对一个简单的操作,我们需要进行以下步骤来完成:

1.声明Action

export const INIT_DATA = Symbol('INIT_DATA');

以上代码其实也只是重复写了一遍字符串,而且还要保证Action类型的唯一性。我们其实没有必要专门在一个文件中定义Action类型,因为 ActionReducer 本质上可以说是一一对应的。RematchModel.reducers.methods 的函数签名反推出Action类型。

  1. 定义一个对应的Action创建函数
export const initConfirmData = () => ({ type: INIT_DATA });
  1. 引入Action,定义Reducer,在复杂的switch语句中,对对象进行更改
import { INIT_DATA, CHANGE_DATA } from 'xxx'

export default (state = initial, action) => {
  switch(action.type) {
    case INIT_DATA:
      return ...
    case CHANGE_DATA:
      return ...
    default:
      return state
  }
}

同省略声明Action类型类似,我们也不希望写冗长的switch语句。本质上,Reducer就是用来进行数据处理,这完全可以在函数中完成。Rematchmodel.reducers.methods的函数体反推出原有switch语句的数据处理逻辑

const counter = {
  state: 1,
  reducers: {
    add: (state, payload) => state + payload,
    sub: (state, payload) => state - payload
  }
}
  1. 在需要时,引入Action创建函数,并将对应的State进行连接
import { initConfirmData } from 'xxx'
@connect(...)

我们只是想做一个简单的状态修改呀!

Rematch将以上技术细节进行了封装,使得开发者只需要通过Dispatch调用一个Action函数就可以修改状态了。

异步处理复杂

目前针对Redux的异步处理较常被使用的解决方案Redux-ThunkRedux-Saga对使用者来说都不是很友好:要么难以理解,要么学习成本过高;Rematch颠覆了这个现状,实现了一个更直观更简洁的异步处理方案。

  1. Redux-Thunk
getFlowCards(index, keyword) => async (dispatch, getState) => {
  //...
  response = await request(URLS.TOUR_FLOW_CARDS, argument)
  if (response) {
    // ...
    dispatch({
      type: actionTypes.GET_TAB_DATA,
      tabData: tabData
    })
    // ...
  }
}

这个Redux-Thunk处理异步任务的常规操作透露着它注定被吐槽“像一个拙劣的黑客方案”的命运。对于使用者来说,有几个地方经常被吐槽:

a.  函数的阶数过多:开发者很难理解为什么异步处理函数就要写成这样的高阶函数
b.  参数不明确:看不出来Dispatch和getState这两个参数是如何被获取到的
c.  会阻断中间件链条:一旦执行了这个异步函数,中间件链条就会被阻断,容易给不熟悉Redux中间件机制的新手挖坑

2. Redux-Saga在处理异步任务上比起Redux-Thunk更具有设计感,不过它引入了另外一个需要开发者去学习的全新的异步模型,有一定学习成本和踩坑风险。

Rematch借鉴Redux-Thunk的思想,对其进行了一定程度的封装,让开发者可以直接使用async/await关键字去实现异步处理。

四、Rematch使用

第一步:初始化

init 用来配置 reducers, devtools & store

// models.ts
import { Models } from '@rematch/core';
import { count } from './count';
export interface RootModel extends Models<RootModel> {
  count: typeof count;
}
export const models: RootModel = { count };
// store.ts
import { init, RematchDispatch, RematchRootState } from '@rematch/core';
import { models, RootModel } from './models';

export const getStore = () =>
  init({
    models,
  });

export type Store = typeof getStore;
export type Dispatch = RematchDispatch<RootModel>;
export type RootState = RematchRootState<RootModel>;
第二步:设置models

models 促使statereducerseffectsasync actions) 放在同一个地方。

// count.ts
import { createModel } from '@rematch/core';
import type { RootModel } from './models';

type Names = 'custom';
type ComplexCountState = {
  count: number;
};

export const count = createModel<RootModel>()({
  state: {
    count: 0,
  } as ComplexCountState,
  reducers: {
    increment(state, payload: number) {
      return {
        count: state.count + payload,
      };
    },
  },
  effects: (dispatch) => ({
    async incrementEffect(payload: number, rootState) {
      console.log('incrementEffect', payload, rootState.count.count);
      await new Promise((resolve) => {
        setTimeout(resolve, 1000);
      });
      dispatch.count.increment(payload);
    },
  }),
});
第三步:设置Dispatch

dispatch 是我们如何在你的model中触发 reducerseffectsDispatch 标准化了你的action,而无需编写action types 或者 action creators

import { dispatch } from '@rematch/core'
                                                  // state = { count: 0 }
// reducers
dispatch({ type: 'count/increment', payload: 1 }) // state = { count: 1 }
dispatch.count.increment(1)                       // state = { count: 2 }

// effects
dispatch({ type: 'count/incrementAsync', payload: 1 }) // state = { count: 3 } after delay
dispatch.count.incrementAsync(1)                       // state = { count: 4 } after delay

第四步:设置View
// app.tsx

import * as React from 'react';
import './style.css';
import { useDispatch, useSelector } from 'react-redux';
import { RootState, Dispatch } from './store';
import { Button } from 'antd';

export default function App() {
  const { count: countState } = useSelector((state: RootState) => state);
  const dispatch = useDispatch<Dispatch>();

  return (
    <div>
      <h1>{`${countState.count}  ${countState.multiplierName}`}</h1>
      <div className="item">
        同步操作
        <Button
          type="primary"
          onClick={() => {
            dispatch.count.increment(1);
          }}
        >
          ➕1
        </Button>
        <Button
          type="primary"
          onClick={() => {
            dispatch.count.increment(-1);
          }}
        >
          ➖1
        </Button>
      </div>

      <div className="item">
        延时操作
        <Button
          type="primary"
          onClick={() => {
            dispatch.count.incrementEffect(1);
          }}
        >
          ➕1
        </Button>
        <Button
          type="primary"
          onClick={() => {
            dispatch.count.incrementEffect(-1);
          }}
        >
          ➖1
        </Button>
      </div>
    </div>
  );
}

五、Rematch原理

Redux 抽象的 ActionReducer 的职责很清晰,Action 负责改 Store 以外所有事,而 Reducer 负责改 Store,偶尔用来做数据处理。这种概念其实比较模糊,因为往往不清楚数据处理放在 Action 还是 Reducer 里,同时过于简单的 Reducer 又要写 Action 与之匹配,感觉过于形式化,而且繁琐。Rematch重新考虑这个问题,它只涉及两类 ActionReducer ActionEffect Action

  • Reducer Action:改变 Store
  • Effect Action:处理异步场景,能调用其他 Action,不能修改 Store

同步的场景,一个 Reducer 函数就能处理,只有异步场景需要 Effect Action 处理掉异步部分,同步部分依然交给 Reducer 函数,这两种 Action 职责更清晰。

我们先来看一下的核心流程图,它概括了Rematch框架的整个运行流程。

image.png

如图,RematchComponents建立起了一个完整的状态管理框架,开发者无需关心过多细节,就可以通过异步或同步请求更新管理状态。下面围绕这个核心运行流程图,讲解一下主要功能:

  • Components:组件实例。负责用户操作等交互行为,执行Dispatch方法触发对应Action进行状态更新。
  • Dispatch:操作行为触发方法,是唯一能执行Action的方法。
  • Actions:用于描述行为。负责处理组件发出/接送的所有交互行为,包括同步和异步行为。Action是在Redux中发送的消息,作为应用程序的不同部分传递状态更新的一种方式。在Rematch中,一个Action始终是一个“Model名称”和“Action名称”类型的结构 - 指的是一个ReducerEffect名称。
  • Plugins:用于扩展和增强Dispatch能力。负责自定义init配置或更改内部hooks,它能添加功能到你的Rematch设置当中来。
  • State:表示页面状态管理容器对象。集中存储页面零散的状态,表示页面的数据层,是整个页面唯一的数据中心,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,该对象与页面显示永远保持一一对应,数据层是另外一种形式的页面显示。

下面我们对Rematch的两个核心模块做下原理解析:

对Redux的封装简化

Rematch封装了Redux的样板代码,以达到简化Redux使用的目的;对应的这部分代码实现主要在reduxStore.tsdispatcher.ts这两个文件中。简单来说,Rematch的封装工作分为两步:

  1. 通过Modelreducers对象来生成Redux需要的Reducer方法

  2. 重新定义Dispatch

    a. 通过Modelreducers对象构造ActionCreator(不再需要手动写ActionAction TypeAction Payload

    b. 将Dispatch这个构造出来的ActionCreator的操作隐藏起来,并映射给函数名为Modelreducers对象的key的一个全新的函数(这就是封装bindActionCreatorsmapDispatchToProps的过程)

    c. 将上面的函数作为Dispatch对象的一个方法

通过reducers对象的key结合model名称得到Action Type,可以把reducers对象的key和对应的function转成Reducer中的一个switch case代码块,可以把reducers对象的key和对应的function经过包装得到一个ActionCreator

其代码实现如下:

第一步,通过Modelreducers对象来生成Redux需要的Reducer方法:
  1. 构造一个新的对象。将ModelReducers对象的key改为${model.name}/${key}的形式(防止其他Model中有重名的key):
for (const modelReducer of Object.keys(model.reducers || {})) {
  const action = isListener(modelReducer)
    ? modelReducer
    : `${model.name}/${modelReducer}`;
  modelReducers[action] = model.reducers[modelReducer];
}
  1. 构造Reducer函数。

    首先,Reducer函数的参数是固定的:包含两个参数;第一个参数是State,第二个参数是Action

    其次,在每个Reducer函数体中执行的是以Action.typekey的上面构造出来的对象的value,并且把stateaction.payload传进去。

const combinedReducer = (state: any = model.state, action: R.Action) => {
  // handle effects
  if (typeof modelReducers[action.type] === 'function') {
    return modelReducers[action.type](state, action.payload, action.meta)
  }
  return state
}
  1. 构造一个总的Reducer对象(this.reducers),这个对象最终会经过combineReducers处理,得到Redux需要的Reducer方法。
this.reducers[model.name] = !modelBaseReducer
  ? combinedReducer
  : (state: any, action: R.Action) =>
      combinedReducer(modelBaseReducer(state, action), action);

for (const model of models) {
  this.createModelReducer(model);
}

以上过程是一个输入输出的过程:

  • 输入:每个ModelReducers节点。Reducers节点下每个key对应的是一个function,这个function一般是包含两个参数:第一个参数是State,第二个参数是Payload
  • 输出:是一个Reducers对象。这个对象的key是每一个Modelnamevalue是一个function,这个function必须包含两个参数:第一个参数是State,第二个参数是Action
第二步,对Dispatch的改造:

遍历model.reducers,给this.dispatch按照model.nameReducerName挂载一个createDispatcher方法。这个方法创建了一个ActionCreator,并且封装Dispatch这个ActionCreator的操作。

onModel(model: R.Model) {
  this.dispatch[model.name] = {}
  if (!model.reducers) {
    return
  }
  for (const reducerName of Object.keys(model.reducers)) {
    // ...
    this.dispatch[model.name][reducerName] = this.createDispatcher.apply(this, [model.name, reducerName])
  }
}

我们来看看createDispatcher的具体实现,它返回了一个函数,在这个函数里Dispatch了一个Action对象。

createDispatcher(modelName: string, reducerName: string) {
  return async (payload?: any, meta?: any): Promise<any> => {
    const action: R.Action = { type: `${modelName}/${reducerName}` }

    if (typeof payload !== 'undefined') {
      action.payload = payload
    }
    if (typeof meta !== 'undefined') {
      action.meta = meta
    }

    return this.dispatch(action)
  }
}

异步任务的处理

在介绍Rematch是如何处理异步任务之前,有必要先从源码角度讲解一下Redux中间件的实现原理。如果没有Redux中间件实现原理的理论基础,理解Rematch异步任务框架的源码将会是一场噩梦。

先看下Redux中的中间件是如何起作用的。

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

可以看到,在第15行,把middleware数组compose之后,传入原始的store.dispatch,经过封装后,拿到最终的Dispatch,再去替换store.dispatch

那么compose里做了什么事呢?这个过程中做了什么事,来利用middleware数组来封装原始的store.dispatch呢?

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

这是利用reduce累加器函数来实现的递归调用,实现的累加就是前一个函数调用后一个函数,那么,

compose([funA, funB, funC])(store.dispatch)

就相当于

funA(funB(funC(store.dispatch)))

这就是中间件的调用过程,实际上这也是一个洋葱模型。经过包装后,我们每次调store.dispatch就相当于要先执行中间件链条,再执行原始的store.dispatch

看懂了applyMiddleware中间件链条的原理,还必须懂得Redux-Thunk是如何处理异步Action的,我们先来看看Redux-Thunk的源码。

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

这里createThunkMiddleware函数return了一个带有三个参数的高阶函数,这个是middleware的约定的函数签名,写自定义中间件,就必须要写成这样,才能被compose起来。

这里的实现非常精简,如果发现传入的Actionfunction,那就执行这个Action并返回这个执行的结果。看到这里,你可能来时没法理解这为什么就能处理异步Action了,我们借助之前中间件的执行流程来讲解。

Redux-thunk作为Redux的一个middleware,只有在middleware里调用next(action)来能把链条串起来,但是我们发现,在Redux-thunk中,一但发现Actionfunction,那就执行这个action,并没有调用next(action),到这里,中间件链条就断掉了。不过,在async Action里一般在异步任务执行完还是会调用dispatch(action),这个时候就会重新走一遍中间件链条。

懂得了这个原理,我们来看看Rematch是如何利用这个原理,实现了只需要async await就可以执行异步Action的。我们来看Rematch内置core plugin中如何处理异步Action的。

middleware : (store: any) => (next: any) => async (action: R.Action) => {
  // async/await acts as promise middleware
  if (action.type in this.effects) {
    await next(action)
    return this.effects[action.type](action.payload, store.getState(), action.meta)
  }
  return next(action)
}

是不是似曾相识?其实就是参考了Redux-Thunk的原理,发现ActionEffects中,就执行这个Action。不过这里跟Redux-thunk是有区别的:

这里是在执行了next(action)后才去执行异步Action的,为什么不像Redux-thunk一样发现是异步Action,那就直接断掉中间件的处理呢?

作为框架,它们不知道后续是否有中间件也能响应那些 Action,所以无脑传递。因为 Reducermiddlewares 都只处理匹配的 Action,否则要么透传,要么返回当前状态。这意味着向后传递是安全的。如果是业务开发的场景,我们明确知道只有我处理这类 Action,那我就不必透传。thunk 里面把控制权交给 Action 函数,它里面调用 Dispatch 就串联起来了。所有函数类型的 Action,都被它拦截,后面不可能有其它中间件做这件事情。

参考文档

rematch.gitbook.io/handbook/

github.com/reduxjs/red…

github.com/rematch/rem…

github.com/dvajs/dva

github.com/mirrorjs/mi…

zhuanlan.zhihu.com/p/374246900