前言
在开始之前先做一下名词解释:
- 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 });
附录
- Yax 项目地址:d-band/yax
- 基于 RN 的一个例子:d-band/RnTotp