背景:工作中经常使用redux,但是一直有个想法,想摸透里面的来龙去脉,虽然知道里面运用了观察者模式,但是苦于没有看源码,一直不知道里面的代码组合,今天终于动手,到redux源码中去徜徉,揣摩优秀的设计,尝试学着造一个简易redux
一般情况下,我们可以通过发布订阅实现一个状态管理器
最简单的状态管理器
let state = {
count: 1
};
let listeners = [];
// substribe
function substribe(listener) {
listeners.push(listener);
}
function changeCount(count) {
state.count = count;
for (let i=0; i<listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
// 订阅一下,count改变时实时输出
substribe(function() {
console.log(state.count);
});
// 修改state 通过changeCount来改变
changeCount(2); // 输出2
changeCount(3); // 输出3
changeCount(4); // 输出4
通过以上代码,就完成了一个极简的状态管理器,主要原理是:发布订阅
这就可以了吗,看看有何问题没?
发现没有:所有的变量都暴露在全局中,可以随意修改,并且只做到控制count
如何解决呢?
想办法把变量都放到函数里面形成局部作用域,只有通过特定的方法(接口)才可以访问里面的变量
怎么做?
变量封装,暴露方法
function createStore(initState) {
let state = initState;
let listeners = [];
// substribe
function substribe(listener) {
listeners.push(listener);
}
// change state
function changeState(newState) {
state = newState;
// trigger
for (let i=0; i<listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return {
substribe,
changeState,
getState
};
}
let initState = {
counter: {count: 0},
info: {name: '', desc: ''}
};
let store = createStore(initState);
store.substribe(() => {
let state = store.getState();
console.log('count: ', state.counter.count);
});
store.substribe(() => {
let state = store.getState();
console.log(`info.name: ${state.info.name}`, `info.desc: ${state.info.desc}`);
});
store.changeState({counter: {count: 1}, info: {name: '', desc: ''}});
store.changeState({counter: {count: 2}, info: {name: 'franki', desc: 'handsome boy'}});
这样就满足了把变量藏匿于函数,不再直接暴露于全局,通过返回的对象,提供对应的接口进行数据修改,并且支持多个状态值(不单单是count)
以上就完了吗?
答案是好戏才刚刚开场
想想还有什么问题呢?
有计划的改变
什么叫做有计划的改变?
function createStore(plan, initState) {
let state = initState;
let listeners = [];
function substribe(listener) {
listeners.push(listener);
}
function changeState(action) {
state = plan(state, action);
for (let i=0; i<listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return {
substribe,
changeState,
getState
};
}
let initState = {
count: 0
};
function plan(state, action) {
switch(action.type) {
case 'INCREMENT':
return {...state, count: state.count+1};
case 'DECREMENT':
return {...state, count: state.count-1};
default:
return state;
}
}
let store = createStore(plan, initState);
store.substribe(() => {
let state = store.getState();
console.log('count: ', state.count);
});
store.changeState({type: 'INCREMENT'});
store.changeState({type: 'DECREMENT'});
到现在,已经可以做到有计划的去修改值,而不是直接通过传入一个具体值,直接赋值,这样的好处是:规定改变数据的方式只有通过特定的方法去触发,并且规定触发的需要传入特定的参数。
做到这样,一个初步的redux已经完工,为了提高逼格,可以把上面代码的plan换成reducer,changeState改为dispatch,这样就跟redux的api一致了
还是那句话,这样就完了吗?
还是那句话,革命尚未成功,同志仍需努力!
想想还有什么方面需要需改?
拆分reducer
上面的代码有个很严重的问题,就是当store树很大的时候,需要维护一个非常臃肿的plan函数,所以拆分plan函数成了首要任务
function createStore(reducer, initState) {
let state = initState;
let listeners = [];
function substribe(listener) {
listeners.push(listener);
}
function dispatch(action) {
state = reducer(state, action);
for (let i=0; i<listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return {
substribe,
dispatch,
getState
};
}
function counterReducer(state, action) {
switch(action.type) {
case 'INCREMENT':
return {...state, count: state.count+1};
case 'DECREMENT':
return {...state, count: state.count-1};
default:
return state;
}
}
function infoReducer(state, action) {
switch(action.type) {
case 'CHANGE_NAME':
return {...state, name: action.name};
case 'CHANGE_DESC':
return {...state, desc: action.desc};
default:
return state;
}
}
function combineReducer(reducers) {
let reducersKeys = Object.keys(reducers);
return function combination(state={}, action) {
// 生成新的state
let nextState = {};
for (let i=0; i<reducersKeys.length; i++) {
const key = reducersKeys[i];
const reducer = reducers[key];
// 获取旧state
const prevStateForKey = state[key];
const nextStateForKey = reducer(prevStateForKey, action);
nextState[key] = nextStateForKey;
}
return nextState;
}
}
const reducers = combineReducer({
counter: counterReducer,
info: infoReducer
});
let initState = {
counter: {count: 0},
info: {name: '', desc: ''}
};
let store = createStore(reducers, initState);
store.substribe(() => {
let state = store.getState();
console.log('count: ', state.counter.count)
});
store.substribe(() => {
let state = store.getState();
console.log(`info.name: ${state.info.name}`, `info.desc: ${state.info.desc}`);
});
store.dispatch({type: 'INCREMENT'});
store.dispatch({type: 'DECREMENT'});
store.dispatch({type: 'CHANGE_NAME', name: 'franki'});
store.dispatch({type: 'CHANGE_DESC', desc: 'handsome boy'});
现在已经拆分完了reducer,已经可以把reducer慢慢细化,划分为一个个小的状态树,符合单一职责的要求
目前为止,redux较为完善
但是还有些瑕疵,下面继续讲解
中间件
function createStore(reducers, initState) {
let state = initState;
let listeners = [];
function substribe(listener) {
listeners.push(listener);
return function unsubstribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
}
}
function dispatch(action) {
state = reducers(state, action);
for (let i=0, l=listeners.length; i<l; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return {
substribe,
dispatch,
getState
}
}
function counterReducer(state={count: 0}, action) {
switch(action.type) {
case 'INCREMENT':
return {...state, count: state.count+1};
case 'DECREMENT':
return {...state, count: state.count-1};
default:
return state;
}
}
function infoReducer(state={name: '', desc: ''}, action) {
switch(action.type) {
case 'CHANGE_NAME':
return {...state, name: action.name};
case 'CHANGE_DESC':
return {...state, desc: action.desc};
default:
return state;
}
}
function combineReducers(reducers) {
let reducersKeys = Object.keys(reducers);
// 处理后返回一个新的reducer
return function(state = {}, action) {
// 新的state
let nextState = {};
for (let i=0; i<reducersKeys.length; i++) {
// 当前key
const key = reducersKeys[i];
// key对应的reducer => counterReducer, infoReducer
const reducer = reducers[key];
// 旧state
const prevStateForKey = state[key];
// 新state
const nextStateForKey = reducer(prevStateForKey, action);
nextState[key] = nextStateForKey;
}
return nextState;
}
}
let initState = {
counter: {count: 0},
info: {name: '', desc: ''}
};
let reducers = combineReducers({
counter: counterReducer,
info: infoReducer
});
let store = createStore(reducers, initState);
let dispatch = store.dispatch;
const loggerMiddleware = action => {
console.log('cur state: ', store.getState());
dispatch(action);
console.log('next state: ', store.getState());
};
store.dispatch = loggerMiddleware;
store.substribe(() => {
let state = store.getState();
console.log('count: ', state.counter.count);
});
store.substribe(() => {
let state = store.getState();
console.log('info.name: ', state.info.name);
console.log('info.desc: ', state.info.desc);
});
store.dispatch({type: 'INCREMENT'});
store.dispatch({type: 'DECREMENT'});
store.dispatch({type: 'CHANGE_NAME', name: 'franki'});
store.dispatch({type: 'CHANGE_DESC', desc: 'handsome boy'});
这样一来,就可以通过中间件去做一些其他事了,上面的例子的中间件是用来记录数据变化,使得数据变化更加有迹可循。
有个问题,上面的中间件也只做了一件事,如果我不但需要记录数据的变化,还需要监控异常怎么办?
你可能想到这样?
const exceptionMiddleware = action => {
try {
loggerMiddleware(action);
} catch (error) {
console.log('something wrong! ', error);
}
};
store.dispatch = exceptionMiddleware;
再来一个统计时间的中间件,你也可能这样?
const timeMiddleware = action => {
const time =
new Date().getFullYear().toString() + '/' +
(new Date().getMonth() + 1).toString() + '/' +
new Date().getDate().toString() + ' ' +
new Date().getHours() + ':' +
new Date().getMinutes() + ':' +
new Date().getSeconds();
console.log('time: ', time);
exceptionMiddleware(action);
};
store.dispatch = timeMiddleware;
确实可以这样实现,但是如此实现,确实不太优雅
如何做到优雅呢?
想办法把各个中间件写成独立的模块,通过参数传递的方式传入想要的值,而不是在代码上硬编码固定
const loggerMiddleware = store => next => action => {
console.log('cur state: ', store.getState());
next(action);
console.log('next state: ', store.getState());
};
const exceptionMiddleware = store => next => action => {
try {
next(action);
} catch (error) {
console.log('something wrong! ', error);
}
};
const timeMiddleware = store => next => action => {
const time =
new Date().getFullYear().toString() + '/' +
(new Date().getMonth() + 1).toString() + '/' +
new Date().getDate().toString() + ' ' +
new Date().getHours() + ':' +
new Date().getMinutes() + ':' +
new Date().getSeconds();
console.log('time: ', time);
next(action);
};
const chain = [exceptionMiddleware, timeMiddleware, loggerMiddleware].map(middleware => middleware(store));
const next = store.dispatch;
chain.map(middleware => {
dispatch = middleware(next);
});
store.dispatch = dispatch;
看懂了没,稍微解释下,每个中间件都是独立的模块,需要的变量都是通过参数的形式传递过来(像是store dispatch action)
chain
的作用是返回一个新的数组
[
next => action => {
try {
next(action);
} catch (error) {
console.log('something wrong! ', error);
}
},
next => action => {
const time =
new Date().getFullYear().toString() + '/' +
(new Date().getMonth() + 1).toString() + '/' +
new Date().getDate().toString() + ' ' +
new Date().getHours() + ':' + new Date().getMinutes() + ':' +
new Date().getSeconds();
console.log('time: ', time);
next(action);
},
next => action => {
console.log('cur state: ', store.getState());
next(action);
console.log('next state: ', store.getState());
}
]
方便好记:
[
exception,
time,
logger
]
接着再次map
目的把dispatch重新赋值,就是利用这行代码
chain.map(middleware => {
dispatch = middleware(next);
});
相当于实现了dispatch = exception(time(logger(dispatch)))
最后把store.dispatch = dispatch
这样一来,只要执行dispatch操作,首先通过exceptionMiddleware => timeMiddleware => loggerMiddleware => dispatch => reducer => newState
以上的代码够完美了吗?
答案是。。还有些不舒服的地方
还可以怎么继续优化?
往下看
上面的例子有个问题是,中间件的聚合都是通过手动拼接,能不能通过一个函数来完成这一系列的操作呢?
// 返回一个dispatch被重写了新的store
function applyMiddleware(...middlewares) {
// 返回一个重写了的createStore
return function reWriteCreateStoreFunc(oldCreateStore) {
// 返回一个新的createStore
return function newCreateStore(reducer, initState) {
// 生成store
const store = oldCreateStore(reducer, initState);
// 按照最小开放原则,中间件,只能拿到getState方法,不容许进行其他操作
const newStore = {getState: store.getState};
let dispatch = store.dispatch;
const chain = middlewares.map(middleware => middleware(newStore));
chain.map(middleware => {
dispatch = middleware(dispatch);
});
store.dispatch = dispatch;
return store;
};
};
}
上面的函数做了这样的一件事,首先把中间件全部收集起来,接着传入旧store函数(createStore),最后返回的是一个新的newCreateStore,利用的新的newCreateStore,传入reducers和initState参数,返回newStore,继续新的substribe和dispatch
眼尖的同学,可能发现
const chain = middlewares.map(middleware => middleware(store));
chain.map(middleware => {
dispatch = middleware(dispatch);
});
store.dispatch = dispatch;
如此实现包裹的dispatch并不优雅,redux官方版本用了compose(组合)实现
// 组合方法 返回a(b(c...))这样形式的结构
function compose(...funcs) {
if (funcs.length === 0) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
利用compose方法可以这样做
const chain = middlewares.map(middleware => middleware(store));
dispatch = compose(...chain)(dispatch);
store.dispatch = dispatch;
可能还有些同学发现
let newStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore)(reducers, initState);
这样会产生一个新的变量,而且与旧store在一起在全局中容易混淆
你还可以这样做
从createStore函数入手,通过参数来判断是直接生成store,还是需要加入中间件之后生成的store
function createStore(reducers, initState, reWriteCreateStoreFunc) {
if (typeof initState === 'function') {
reWriteCreateStoreFunc = initState;
initState = undefined;
}
if (reWriteCreateStoreFunc) {
const newCreateStore = reWriteCreateStoreFunc(createStore);
return newCreateStore(reducers, initState);
}
let state = initState;
let listeners = [];
function substribe(listener) {
listeners.push(listener);
return function unsubstribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
function dispatch(action) {
state = reducers(state, action);
for (let i = 0, l = listeners.length; i < l; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
return {
substribe,
dispatch,
getState
};
}
// 调用的时候这样写
let store = createStore(reducers, initState, applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware));
// 还可以不传initState
let store = createStore(reducers, applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware));
// 或者initState和中间件相关都不传
let store = createStore(reducers);
这样的话,就不用管理多个store了
redux官方还提供replaceReducer方法,这个是如何做的呢?
// 在createStore方法加入
function replaceReducer(newReducer) {
// 把新reducer赋予旧的reducer达到替换的效果
reducer = newReducer;
// 重新dispatch达到更换效果
dispatch({type: Symbol()});
}
// 接着返回
return {
...
replaceReducer
}
// 这样使用
// 生成新的reducer并替换掉旧的reducer
const newReducers = combineReducers({
counter: counterReducer,
});
store.replaceReducer(newReducers);
store.dispatch({ type: 'INCREMENT' });
做完以上的所有事情一个完整的redux浮出水面
下面放上完整的redux代码,如果嫌长,可以自行划分模块处理
function createStore(reducers, initState, reWriteCreateStoreFunc) {
if (typeof initState === 'function') {
reWriteCreateStoreFunc = initState;
initState = undefined;
}
if (reWriteCreateStoreFunc) {
const newCreateStore = reWriteCreateStoreFunc(createStore);
return newCreateStore(reducers, initState);
}
let state = initState;
let listeners = [];
function substribe(listener) {
listeners.push(listener);
return function unsubstribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
function dispatch(action) {
state = reducers(state, action);
for (let i = 0, l = listeners.length; i < l; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
function replaceReducer(newReducer) {
// 把新reducer赋予旧的reducer达到替换的效果
reducer = newReducer;
// 重新dispatch达到更换效果
dispatch({ type: Symbol() });
}
dispatch({ type: Symbol() });
return {
substribe,
dispatch,
getState,
replaceReducer
};
}
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
function infoReducer(state = { name: '', desc: '' }, action) {
switch (action.type) {
case 'CHANGE_NAME':
return { ...state, name: action.name };
case 'CHANGE_DESC':
return { ...state, desc: action.desc };
default:
return state;
}
}
function combineReducers(reducers) {
let reducersKeys = Object.keys(reducers);
// 处理后返回一个新的reducer
return function(state = {}, action) {
// 新的state
let nextState = {};
for (let i = 0; i < reducersKeys.length; i++) {
// 当前key
const key = reducersKeys[i];
// key对应的reducer => counterReducer, infoReducer
const reducer = reducers[key];
// 旧state
const prevStateForKey = state[key];
// 新state
const nextStateForKey = reducer(prevStateForKey, action);
nextState[key] = nextStateForKey;
}
return nextState;
};
}
let initState = {
counter: { count: 0 },
info: { name: '', desc: '' }
};
let reducers = combineReducers({
counter: counterReducer,
info: infoReducer
});
const loggerMiddleware = store => next => action => {
console.log('cur state: ', store.getState());
next(action);
console.log('next state: ', store.getState());
};
const exceptionMiddleware = store => next => action => {
try {
next(action);
} catch (error) {
console.log('something wrong! ', error);
}
};
const timeMiddleware = store => next => action => {
const time =
new Date().getFullYear().toString() + '/' +
(new Date().getMonth() + 1).toString() + '/' +
new Date().getDate().toString() + ' ' +
new Date().getHours() + ':' +
new Date().getMinutes() + ':' +
new Date().getSeconds();
console.log('time: ', time);
next(action);
};
function compose(...funcs) {
if (funcs.length === 0) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
// 返回一个dispatch被重写了新的store
function applyMiddleware(...middlewares) {
// 返回一个重写了的createStore
return function reWriteCreateStoreFunc(oldCreateStore) {
// 返回一个新的createStore
return function newCreateStore(reducer, initState) {
// 生成store
const store = oldCreateStore(reducer, initState);
const newStore = { getState: store.getState };
let dispatch = store.dispatch;
const chain = middlewares.map(middleware => middleware(newStore));
// chain.map(middleware => {
// dispatch = middleware(dispatch);
// });
dispatch = compose(...chain)(dispatch);
store.dispatch = dispatch;
return store;
};
};
}
let store = createStore(reducers, initState, applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware));
store.substribe(() => {
let state = store.getState();
console.log('count: ', state.counter.count);
});
store.substribe(() => {
let state = store.getState();
console.log('info.name: ', state.info.name);
console.log('info.desc: ', state.info.desc);
});
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'CHANGE_NAME', name: 'franki' });
store.dispatch({ type: 'CHANGE_DESC', desc: 'handsome boy' });
// 生成新的reducer并替换掉旧的reducer
const newReducers = combineReducers({
counter: counterReducer
});
store.replaceReducer(newReducers);
store.dispatch({ type: 'INCREMENT' });
码字不易
小小总结下,redux的完整流程
views > store.dispatch > store (reducer state) > views
接着详细解释下,redux里面用到的一些重要名词
- createStore 创建store对象,包含substribe dispatch getState replaceReducer
- reducer 是一个计划函数,接收旧state 和 action,用于改变state树
- dispatch 用于触发action,生成新state
- substribe 订阅功能,每次触发dispatch的时候,会执行订阅函数
- combineReducers 将多个reducer合并成一个reducer
- replaceReducer 替换 reducer 函数
- middleware 用于扩展dispatch方法