简单温故
React最经典的状态管理库非redux莫属。
简单的说,redux在flux单项数据流思想上又限制了三大原则:单一数据源、纯函数改变值、只读状态(只能用action去修改)。dva.js可以说算是react中redux使用过程中标准化流程的封装(号称是:最佳实践)。
在一步一步实现redux之前,可以简单温故下redux:
https://redux.js.org/introduction/getting-started
1. 最基本状态管理
作为状态管理库,最核心的就是管理状态,实现的第一步做这几件事:
- 实现一个基本的store
- 发布/订阅模式可以监听到数据变化
输出getState、subscribe、changeState
function createStore(initState = {} ) {let state = initState;// 订阅者const listners = [];// 得到当前数据const getState = () => {return state;}// 订阅数据// lis是一个函数const subscribe = (lis) => {listners.push(lis);}// 数据变化调用此函数// 当数据变化后,遍历订阅者,调用之const changeState = (newState) => {state = newState; // 变化数据listners.forEach(lis => lis()); // 通知}return {getState,subscribe,changeState,}}
这样一个createStore就基本实现了最简单版本的redux数据管理的要求,当然很简陋,测试下:
const initState = {name: 'yxnne'};const store = createStore()const listner1 = () => {console.log('in lisner1 :', store.getState())}const listner2 = () => {console.log('in lisner2 :', store.getState())}store.subscribe(listner1);store.subscribe(listner2);setTimeout(() => {store.changeState({...store.getState(),name: 'super yxnne'});}, 2000)
2. 使用reducer
上一步只完成了数据的存储和订阅,但在redux中有一个很重要的原则就是改变数据只能通过reducer来修改数据,所以这一步,需要做这样的处理。
reducer是什么东西呢,其输入是action、 即将被修改的状态值,输出是新的状态值
// 若用TS定义的话就是这样function reducer(action: string): IState;
借助reducer的话,自然是不需要上面的 changeState 函数了。但是需要提供一个dispatch函数,用来给reducer派发action,大致就是这样:
function dispatch(action) {const newState = reducer(state, action);// 通知订阅者}
这样的话createStore就需要修改成:
function createStore(reducer, initState = {} ) {let state = initState;// 订阅者const listners = [];// 得到当前数据const getState = () => {return state;}// 订阅数据// lis是一个函数const subscribe = (lis) => {listners.push(lis);}// 使用dispatch// 派发actionconst dispatch = (action) => {const newState = reducer(state, action);state = newState;listners.forEach(lis => lis());}return {getState,subscribe,dispatch,}}
使用的时候就是,需要自定义一个reducer:
const reducer = (state, action) => {let newState = {};switch (action) {case 'add_age':newState = { ...state, age: 30break;default:// ....return newState}
在createStore需要把reducer传进去:
// createStore需要把reducer传进去const store = createStore(reducer, initState)setTimeout(() => {// 派发action用以改变数据store.dispatch('add_age');}, 2000)
3. 合并reducer
接下来需要处理的情况是,当有多个reducer的时候,将reducer合并起来。(实际使用中通常会把相关的数据写成一个reducer,所以通常情况下项目中的reducer可不止一个)
所以需要实现combineReducers,先分析之:
- 多个reducer合并成一个,那么reducer其实是一个函数,那么合并多个reducer之后也应该是个函数;
- 使用了combineReducers之后,redux的单一数据源还是单一数据源,只不过增加了一层‘索引’,即reducer key, 或者说是类似namespace的东西。这一点就要求我们做合并reducer的时候,对combineReducers传参数用对象,key是索引,value是reducer。
最终使用起来应该是:
const finalReducer = combineReducers({user: userReducer, // namespace userproduct: productReduer, // namespace product});const store = createStore(finalReducer, initState);// ....略
那么,实现combineReducers如下:
const combineReducers = (allReducers) => {return (state, action) => {const nextState = {};Object.keys(allReducers).forEach(key => {const reducer = allReducers[key];// previousStateForKey:// 某namespace为key的值的先前值const previousStateForKey = state[key];// 改变后的值是执行reducernextState[key] = reducer(previousStateForKey, action);});return nextState;}}
上面这个函数combineReducers中,返回的是一个函数,这个函数的如参和reducer需要一致。
看下所谓合并的过程,其实就是对每一个action调用所有的reducer,当然,没有命中reducer,自然是走了其default的逻辑了;
另外,这个合并后函数的输出确如上文所说那样,相当于合并了所有的reducer的namespace对应的state,组成一个大的对象。
比如现在有这两个reducer:
// userconst userReducer = (state = {list:[]}, action) => {let newState = {};switch (action.type) {case 'user_add':newState = {...state,list: [ ...state.list, action.payload]}break;default:newState = statebreak;}return newState}// productconst productReducer = (state = {list:[]}, action) => {let newState = {};switch (action.type) {case 'prod_clear':newState = {...state,list: []}break;default:newState = statebreak;}return newState}
合并reducer并初始化store:
const initState = {user: { list: [] },product: { list: [{id: '1'}, { id: 2 }] }}// 合并const reducer = combineReducers({user: userReducer,product: productReducer});const store = createStore(reducer, initState);// ....后续逻辑和之前一样的
这样就完美了combine部分就没啥问题了~
不过这里需要增强下健硕性:当createStore中initState不传,那程序会崩溃的:因为在combineReducer中:nextState[key] = reducer(previousStateForKey, action); 这里的previousStateForKey 是undefined了,那么真正执行到reducer中逻辑会报错。
避免这个问题,就要求在写reducer的时候一定要给出默认state:
const productReducer = (state = {list:[]}, action) => { }
另外,在createStore返回结果之前,触发一次初始化过程,这样在逻辑上更有完整性,相当于我们在开始之前,先把整个默认数据构造好了:
dispatch({ type: Symbol() });
中间件
分析中间件
个人理解,中间件这种思想是AOP思想的一种落地方式。从设计模式的角度上属于责任链模式。
先简单看下redux中间件,分析下它,比如现在有一个中间件loggerMiddleware,作用是记录日志:
const loggerMiddleware = (store) => (next) => (action) => {console.log(`before ${action} state :>`, store.getState());next(action);console.log(`after :>`, store.getState());};
两年前,第一眼看到这个东西我的第一反应就是:什么鬼…. “(store) => (next) => (action) => ”后面知道这种形式和函数被currying后的形式。
首先看下中间件内部,在调用next前后都输出了state值。
其次,这个函数单看是看不出什么的,需要结合使用,一看就明白了:
const next = store.dispatch;const logger = loggerMiddleware(store);// 最关键的一步store.dispatch = logger(next);
最关键的一步,核心就是新的函数(含有中间件功能的函数)需要替换dispatch。但是,最终还是要调用dispatch的,所以,对于就一个中间件logger来讲,next就是原来的dispatch。
那么,考虑更复杂的情况:假设现在不止logger,还有timer,auth——即更多的中间件:
const logger = loggerMiddleware(store);const timer = timerMiddleware(store);const auth = authMiddleware(store);
假设现在就用这三个中间件,该如何替换dispatch呢?
const next = store.dispatch;// 这样搞store.dispatch = auth(timer(logger(next)));
就像上面代码这样,按照期望的调用顺序,将中间件嵌套调用,内层的就是外层的next,而最内层的next自然是原来的dispatch。
代码如预期般丝滑柔顺~
compose
上面成功的实现了中间件机制,虽说功能无恙,但是不够优雅。
redux针对中间件有处理方法:applyMiddleware; 简单阐述下原理,applyMiddleware的入参就是需要应用的中间件,依次传入;applyMiddleware返回一个接受createStore的函数,在这个函数入参和createStore一致,因为内部对createStore进行了调用,并且改变了dispatch(借助compose合并中间件将dispatch赋给合并后的函数),返回值和createStore也保持一致。
const applyMiddleware = function (...middlewares) {return function (oldCrateStore) {return function (reducer, initState) { // 此函数就是包裹的createStore// 得到原始的storeconst store = oldCrateStore(reducer, initState);const simpleStore = { getState: store.getState };// 用store 初始化 中间件const chain = middlewares.map((middleware) => middleware(simpleStore));// 合并中间件,并生成新的dispatchconst dispatch = compose(...chain)(store.dispatch);return {...store,dispatch,};};};};
这里面用了compose,compose很简单,借助reduce方法:
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)));}
最后,applyMiddleware要使用,还需要配合改写下createStore,不需要做很多事情,就是参数中新加一个参数,类型是函数,然后函数体中判断下是不是有这个参数,如果有,执行此参数,利用其返回函数生成新的store:
export default function createStore(reducer,initialState,rewriteCreateStoreFunc) {if (rewriteCreateStoreFunc) {// 生成新的createStore方法const newCreateStore = rewriteCreateStoreFunc(createStore);// 新的createStore方法中会调用createStore// 因为没有给参数rewriteCreateStoreFunc// 所以在调用createStore的时候不会进入:if (rewriteCreateStoreFunc)return newCreateStore(reducer, initialState);}// ...略 (后面一点没改)}
使用的时候,这样使用:
const rewriteCreateStoreFunc = applyMiddleware(loggerMiddleware,timerMiddleware,authMiddleware)const store = createStore(reducer, initState, rewriteCreateStoreFunc);
顺理成章,点到为止~
thunk
针对redux的中间件已经完全实现了。
最后讨论一个经典的异步中间件方案——thunk,redux-thunk。
回忆下如何使用redux-thunk?
// in React onCkick handleconst handleAddClick = () => dispatch(addAsync);// .....function addAsync() {return (dispatch) => {setTimeout(() => {dispatch({type: 'ADD',});}, 1000);};}
比如有一个点击事件的处理函数handleAddClick,这个函数调用了 dispatch(addAsync), addAsync返回了一个函数,这个函数中存在异步逻辑,上文代码的异步逻辑是定时任务,当任务结束后,再派发一个同步任务。
在查看redux-thunk的源码前,先解释下原理,redux-thunk首先是个中间件,在这个中间件内部做了一个逻辑就是,拦截下action是函数类型的action,执行这个action;那么其它不是函数类型的action继续走next,执行后续中间件。
源码很简洁:
function createThunkMiddleware(extraArgument) {// 中间件接口 store => next => actionreturn ({ dispatch, getState }) => (next) => (action) => {// action要是函数// 拦截并执行// 不走next了if (typeof action === 'function') {return action(dispatch, getState, extraArgument);}return next(action);};}const thunk = createThunkMiddleware();thunk.withExtraArgument = createThunkMiddleware;export default thunk;
最后一个问题,什么是thunk?
A thunk is a function that wraps an expression to delay its evaluation.(thunk就是一个函数包裹了一个用以延迟求值的表达式)
// 立即执行 1 + 2let x = 1 + 2;// 1 + 2 的执行被延迟了// 因为foo可以稍后被调用// foo 就是一个 thunk!let foo = () => 1 + 2;
从这个角度上来看redux-thunk。还是上文的例子:
function addAsync() {return (dispatch) => {setTimeout(() => {dispatch({type: 'ADD',});}, 1000);};}
再去理解下,其实本质是想执行的是(只不过不是现在):dispatch({ type: ‘ADD’, })。所以我们站在redux的立场上,就是借助addAsync的派发,从而延迟的派发了这个type是ADD的action。
redux和函数式编程思想
搞完了redux最核心的东西,回头从函数式编程的角度,看看redux。
redux这个东西体现了函数式编程的思想:
函数式编程讲究数据放入中容器,通过菡子对容器中的数据做映射,从而改变数据。
那么这些思想对应到redux中,store就是容器,映射(map)就是reducer,而函子就是各种各有的action~
另外,看redux源码我们可以看到全部是函数式编程的概念:currying、compose、reduce、高阶函数、thunk….
总之,redux很经典,函数式编程是个活儿~
