简单温故
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
// 派发action
const 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: 30
break;
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 user
product: 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];
// 改变后的值是执行reducer
nextState[key] = reducer(previousStateForKey, action);
});
return nextState;
}
}
上面这个函数combineReducers中,返回的是一个函数,这个函数的如参和reducer需要一致。
看下所谓合并的过程,其实就是对每一个action调用所有的reducer,当然,没有命中reducer,自然是走了其default的逻辑了;
另外,这个合并后函数的输出确如上文所说那样,相当于合并了所有的reducer的namespace对应的state,组成一个大的对象。
比如现在有这两个reducer:
// user
const userReducer = (state = {list:[]}, action) => {
let newState = {};
switch (action.type) {
case 'user_add':
newState = {
...state,
list: [ ...state.list, action.payload]
}
break;
default:
newState = state
break;
}
return newState
}
// product
const productReducer = (state = {list:[]}, action) => {
let newState = {};
switch (action.type) {
case 'prod_clear':
newState = {
...state,
list: []
}
break;
default:
newState = state
break;
}
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
// 得到原始的store
const store = oldCrateStore(reducer, initState);
const simpleStore = { getState: store.getState };
// 用store 初始化 中间件
const chain = middlewares.map((middleware) => middleware(simpleStore));
// 合并中间件,并生成新的dispatch
const 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 handle
const 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 => action
return ({ 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 + 2
let 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很经典,函数式编程是个活儿~