Make redux like vuex

676 阅读3分钟

前言

在开始之前先做一下名词解释:

  • vuex: Centralized State Management for Vue.js
  • redux: Predictable state container for JavaScript apps

简单点的解释它们都是用来管理状态的库,只不过 vuex 是给 Vue 用的,redux 是给 JS App 用的(但是大部分都是给 React 用的,所以可以说是给 React 用的)

看过两者文档的人应该都有感触,vuex 的接口非常的简单,以至于看一遍 Getting Started 就可以上手写代码了。相比之下 redux 的文档很多人读了 N 多遍也不知道:reducer 应该如何拆分、action 应该怎么定义、dispatch 异步怎么做、Ajax 怎么使用、middleware 什么时候需要用、enhancer 干什么的、高阶函数怎么这么多 等等一系列问题。

于是支付宝总结自己的前端应用架构推出了开源框架 dva,它的定位:dva 是 framework,不是 library。它明确定义了各个部件应该如何写,对于团队开发便于统一接口,但却少了一些自由度,比如内置的 redux-saga 和 react-router 目前是无法替换掉的,而且 saga 是 dva 的基石,但是其实异步用 saga 有点复杂了,react-router 在 dva 里也无法用于 ReactNative。

开始

对 vuex、redux 和 dva 有了简单的了解后,我们可以开始了:参考 vuex 和 dva 写一个基于 redux 的状态管理库。首先给这个库起个名字 Yax (Yet another store using redux)并且定位是仅仅是一个 library。

第一步:定义接口(参考 vuex)


yax(options = { state, reducers, actions, modules })

阅读 vuex 源码(store.js)


class Store {
  constructor (options = {}) {
    ...
    this._modules = new ModuleCollection(options)
    ...
    installModule(this, state, [], this._modules.root)
  }
  registerModule (path, rawModule) {...}
  unregisterModule (path) {...}
}
function installModule (store, rootState, path, module, hot) {
  ...
  const local = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, [...path, key], child, hot)
  })
}

可以看到 vuex 通过 ModuleCollection 来管理树形结构的 Module,因此我们可以仿照这个来写一下我们的实现:


function yax (options = {}) {
  let _actions = {};
  let _modules = new ModuleCollection(options);
  
  // 初始化 root module 和 sub-modules
  _installModule([], _modules.root);
  // 创建 redux store
  const _store = createStore(
    _modules.makeReducers(),
    options.state || {},
    applyMiddleware(middleware)
  );

  function registerModule (path, rawModule) {
    // 在 ModuleCollection 里添加相应的 module
    _modules.register(path, rawModule);
    // 安装 Module
    _installModule(path, _modules.get(path));
    // 重置 redux 的 reducer
    _resetReducers();
  }

  function unregisterModule (path) {
    // 在 ModuleCollection 里删除相应的 module
    _modules.unregister(path);
    // 重置 _actions 并 重置 redux 的 reducer
    _resetStore();
  }

  return {
    registerModule,
    unregisterModule,
    ..._store
  };
}

如何实现 _installModule 呢?根据上面可以大体设计一下:makeReducers 返回所有模块的 reducer,因此在这之前要把各个模块自己的 reducer 准备好,然后遍历树形结构合并所有的 reducer;相应的 _actions 的映射也应该在 _installModule 这个阶段完成。


function _installModule (path, module) {
  const namespace = _modules.getNamespace(path);

  const local = _makeLocalContext(namespace, path);
  // 清空模块内的 reducer
  module.cleanReducers();
  // 注册模块的 reducer
  module.forEachReducer((reducer, key) => {
    const actionType = namespace + key;
    module.addReducer(actionType, reducer);
  });
  // 注册 action
  module.forEachAction((action, key) => {
    const actionType = namespace + key;
    _registerAction(actionType, action, local);
  });
  // 注册子模块
  module.forEachChild((child, key) => {
    _installModule([...path, key], child);
  });
}

第二步:定义 Middleware


// 定义 middleware 处理 action
const middleware = () => (next) => (action) => {
  if (isObject(action)) {
    const { type, payload } = action;
    const entry = _actions[type];
    // 如果 action 存在则执行
    if (entry) {
      return entry.length > 1
        ? Promise.all(entry.map(handler => handler(payload)))
        : entry[0](payload);
    }
  }
  return next(action);
}; 

第三步:生成 reducer(Module 内部)


makeReducers () {
  return (state = this.state, action) => {
    const { type, payload } = action;
    const handler = this._reducers[type];
    // 处理本模块的 reducer
    if (handler) {
      return handler(state, payload);
    }
    // 处理子模块的 reducer
    if (Object.keys(this._children).length) {
      const tmp = {};
      this.forEachChild((child, key) => {
        tmp[key] = child.makeReducers();
      });

      return combineReducers(tmp)(state, action);
    }
    return state;
  };
}

如何使用

上面包含了大部分的关键代码,更多的细节参见:d-band/yax,最终的接口为:


yax(options = { state, reducers, actions, modules }, enhancer)

简单的 Demo:


import yax from 'yax';

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

const count = {
  state: 0,
  reducers: {
    addDone (state, payload) {
      return state + payload;
    },
    minusDone (state, payload) {
      return state - payload;
    }
  },
  actions: {
    async add ({ commit }, payload) {
      await delay(1);
      commit('addDone', payload);
    },
    async minus ({ commit }, payload) {
      await delay(1);
      commit('minusDone', payload);
    }
  }
};
const store = yax({
  modules: { count }
});

store.subscribe(() => console.log(store.getState()));

store.dispatch({ type: 'count/add', payload: 2 });
store.dispatch({ type: 'count/minus', payload: 1 });

附录