redux核心功能实现
前言
redux是一个十分常用js的状态管理库,但并不意味着一定为react服务,真正把redux和react绑在一起的是react-redux。
redux里边有很多概念,比如store,action,reducer,dispatch。
这些概念也和很多模板语法相绑定,比如更新状态需要派发一个action,而每个action必须要有一个type属性。
reducer中根据action的type类型进行不同的业务逻辑处理,最终返回新状态完成状态变更。
猛然一看,整个更新链路还是很长的。这也是redux被吐槽的点吧,模板语法太多,显得冗余。
但不管怎么说,redux还是很优秀的。站在面试角度,涉及的redux核心点其实还好,主要是createStore,compose。
当然也有一些其他的,面试不太常考但确实是加分项的bindActionCreators,
combineReducers,applyMiddleware。
本篇为源码系列核心实现第三篇,对应下图redux部分。
name | desc |
createStore | 创建store |
compose | 组合中间件(同为洋葱模型) |
bindActionCreators | 为actionCreator绑定dispatch |
combineReducers | 合并多个reducer |
applyMiddleware | 应用中间件 |
createStore到底干了点什么?
基本使用
import {createStore} from 'redux'
const store=createStore(reducer,initState)
如果你实际用过redux,那对上面的代码一定不陌生。
createStore接收reducer,initState为参数,返回一个创建后的store。
其实还有第三个参数enhancer,不过在本文不做重点。
大胆的猜一下,createStore的背后,偷偷的干了些什么呢?
又或者说,你觉得,它应该在这个阶段做什么?
内部实现
createStore是一切状态来源,从这点考虑,它内部必然涉及状态的初始化。
此外,说到状态,那自然要有状态获取和状态更新。
而createStore内部,其实也就干了这几件事。
const createStore = (reducer, preloadState) => {
//状态初始化
let currentState = preloadState
let currentListeners = [];
//获取状态
function getState() {
return currentState;
}
//更新状态
const dispatch = action => {
currentState = reducer(currentState, action);
currentListeners.forEach((l) => l());
return action;
};
// initState 初始状态树构建
//其实这里type是什么没太大意义,主要是为了初始化
dispatch({ type: 'INIT' })
//订阅
const subscribe = listener => {
currentListeners.push(listener);
return function () {
let index = currentListeners.indexOf(listener);
currentListeners.splice(index, 1);
};
};
const store = {
getState,
dispatch,
subscribe,
};
return store;
};
关键点都有注释,应该好理解。至于subscribe,暂且略过,下一篇结合react-redux就好理解了。
面试会问什么呢?哈哈,结合我的经验,大概会问你redux中用了什么设计模式?
subscribe很明显的,发布订阅模式。
反过来想,如果问你知道什么设计模式,你也可以说发布订阅,并举例redux中createStore的实际应用。
此外,如果你能手写createStore并指明发布订阅具体是哪里体现的,那绝对是加分项。
好用的bindActionCreators
bindActionCreators解决了什么问题?
我关注源码除了解决实际业务痛点和面试外,还有一点就是可以找到那些不太为人所知但确实很有用的小东西。
就比如bindActionCreators。它将actionCreator与dispatch完成绑定,简化了redux的更新逻辑代码。
下一步探索前,先解释2个小概念,以防理解混乱。
name | desc | example |
action | 带有type属性的js简单对象 | const action={ type:‘xxx’} |
actionCreator | 一个返回action的函数 | const ac=()=>({ type:‘xxx’}) |
之前的写法
ok,在引出bindActionCreator前,我们先看一下以前更新逻辑是如何的。
const add=(msg)=>({type:'add',msg})
<button onClick={()=>{store.dispatch(add('add'))}}>add</button>
代码看似很简单,但更新部分很冗余,每个更新都要加store.dispatch。
这东西能不能一次绑定,随处使用?bindActionCreator就是干这事用的。
function bindActionCreator(actionCreator, dispatch) {
return function(...args) {
return dispatch(actionCreator(...args))
}
}
现在的写法
//有参场景
const add=bindActionCreator(add,store.dispatch)
<button onClick={()=>add('add')}>add</button>
//无参场景
const add=bindActionCreator(add,store.dispatch)
<button onClick={add}>add</button>
bindActionCreator本质就是将store.dispatch做了一个收敛,内部绑定actionCreator。
这样一来,不论是有参数还是无参数情况,都看着简洁了些。如果是多绑定,效果更明显。
bindActionCreators实现与应用
function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === "function") {
return bindActionCreator(actionCreators, dispatch);
}
const boundActionCreators = {};
for (const key in actionCreators) {
const actionCreator = actionCreators[key];
if (typeof actionCreator === "function") {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
}
}
return boundActionCreators;
}
// 绑定多个ActionCreator
const increment = () => ({ type: INCREMENT})
const decrement = () =>({ type: DECREMENT })
const actionCreators = {
increment,
decrement,
};
const bindActions = bindActionCreators(actionCreators, store.dispatch);
<button onClick={bindActions.increment}>+</button>
<button onClick={bindActions.decrement}>-</button>
如果你注意看bindActionCreators的实现,它其实是和传入的actionCreator同名绑定。
combineReducers
不论哪种形式的更新,最后都会走到reducer去处理。
如果一个项目只有一个reducer,随着项目复杂度的提升,都写在一起会很难维护。
更好的方法是先拆后合。根据业务不同,可细分多个reducer,单独维护自己的小状态,最后合并在一起。
combineReducers就是用来干这件事的,它接收一个包含reducer的对象,并返回一个合并后的reducer。
为了更便于理解,文末我会放一个实战项目,请注意查收。
//获取各自对应的state和reducer,完成更新后返回新的state
function combineReducers(reducers) {
return function (state, action) {
const nextState = {};
for (const key in reducers) {
if (reducers.hasOwnProperty(key)) {
let reducer = reducers[key];
let preStateForKey = state[key];
let nextStateForKey = reducer(preStateForKey, action);
nextState[key] = nextStateForKey;
}
}
return nextState;
};
}
中间件
截止到目前,redux核心已经讲了一半,接下来是中间件的组合与应用。
它并不神奇,但是有些难理解,建议多写代码实践下。
对应源码中的applyMiddleware和compose。
中间件是什么?
顾名思义,中间件就是中间环节的处理过程,可以做一些额外的事。
那什么算是中间环节呢?对redux来说,派发action到状态更新完毕整个过程中,
任意节点都算是中间环节。也可以看做是工厂流水线,在中间环节不断加工。
先来个最好理解的logger中间件,在每次状态变更之后打印新状态。
const originDispatch = store.dispatch;
store.dispatch = (action: any): any => {
console.log("before:", store.getState());
originDispatch(action);
console.log("after:", store.getState());
return action;
};
就这?就算是一个中间件了?是的,简化版可以这么理解。
如果想更进阶一层,可以这样写。
const logger = function (api) {
return function (next) {
return function (action) {
console.log("before:", api.getState());
let res = next(action);
console.log("after:", api.getState());
return res;
};
};
};
为什么要写成这样?api,next又是什么?这些疑问会在applyMiddleware中给出答案,莫急。
中间件的原理是什么?
一句话,使用自定义逻辑的dispatch替换store中的dispatch,且在自定义的dispatch内部调用store.dispatch。
有点绕,也有点像react中的自定义hooks,但拆开来看也许好理解些。
重写dispatch是为了自定义我们想要的逻辑,比如状态打印。
而redux对外只提供了一种更新方式,那就是dispatch。
所以我们需要保留原始的dispatch用于触发更新。
为了便于理解,再举一个派发异步action的例子。
默认redux是不支持的,需要借助thunk,saga等异步中间件处理。
这里来个简化版的异步处理:
const originDispatch = store.dispatch;
store.dispatch = action=> {
// 也可以根据action.type决定是否需要延迟处理
setTimeout(() => {
// 外部先延迟处理
// 这里调用改写前的store.dispatch
// 本质还是同步
originDispatch(action);
}, 1000);
return action;
};
applyMiddleware
applyMiddleware用于应用中间件,其流程相对繁杂。
首先,applyMiddleware接收中间件为参数,函数调用后返回一个storeEnhancer。
其次,storeEnhancer接收createStore为参数,返回storeEnhancerStoreCreator。
最后,storeEnhancerStoreCreator 接收reducer和initState, 返回store。
const storeEnhancer = applyMiddleware(logger);
const storeEnhancerStoreCreator = storeEnhancer(createStore);
const _store = storeEnhancerStoreCreator(rootReducer, {
counter1: { number: 0 },
counter2: { number: 0 },
});
applyMiddleware实现
function applyMiddleware(...middlewares) {
return (createStore) => {
return (...args) => {
let store = createStore(...args);
let dispatch = () => {};
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
};
//注意这里传递的middlewareAPI
//其实就是中间件执行时候接收的api参数
//直接解构可以得到getState函数和dispatch函数
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
//此时的dispatch已经是应用完中间件后的dispatch
return {
...store,
dispatch,
};
};
};
}
上边代码需要额外关注的是下边这两行,本质都是在进行绑定,或者说,柯里化。
// chain = [(next)=>(action)=>{…}, (next)=>(action)=>{…}, (next)=>(action)=>{…}]
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
最原始的中间件其实有三层api=>next=>action=>{},一直到最后一层才真正派发action。
在map执行完,已经拆出了一层。chain实际内容已在上边注释中标出,api是什么在applyMiddleware中标出。
那问题来了,next是什么?
在解释前,需要先看下compose函数。
compose
看起来compose有些难理解,一句话,从右至左依次将右边的返回值作为左边的参数传入。
不得不佩服redux作者,这波思路实在是有点秀。
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)));
}
组合了什么?看起来似乎还是不太好理解。来个小例子,说明组合的效果。
function m1(a) {
return '冷' + a
}
function m2(b) {
return '月' + b
}
function m3(c) {
return '心' + c
}
//从右到左依次执行
//返回的结果用作下一个函数的参数
m1(m2(m3('1024')))//冷月心1024
这个时候再看compose(…chain)(store.dispatch),是否有些感觉了?
实际上如果只有一个中间件,那么next实际就是指的传入的参数,即store.dispatch。
如果是多个中间件,next指向的就是下一个中间件。
回首再看logger中间件。
const logger = function (api) {
//next代表的是调用下一个中间件或者原来的store.dispatch
return function (next) {
//这里返回的函数就是compose最终组合后的结果,即改写后的dispatch
//至于为什么要改写,最开始中间件原理有提到
//自定义逻辑就必须改写,保证能触发更新就必须调用旧的dispatch
return function (action) {
console.log("before:", api.getState());
let res = next(action);
console.log("after:", api.getState());
return res;
};
};
};
redux-thunk
顺路看一眼thunk的实现,重在思想,代码其实不多。
在Redux中默认action必须是一个简单对象,其中不包括函数和数组,且默认只能处理同步逻辑。
因为异步的刚派发过去还没拿到返回结果reducer已经完事了,显然不符合预期。
当我们使用redux-thunk后,可以dispatch一个函数,然后在其内部写异步派发逻辑。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
//如果是函数 执行后返回
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
//next为之前传入的store.dispatch,即改写前的dispatch
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
再会
情如风雪无常,
却是一动既殇。
感谢你这么好看还来阅读我的文章,
我是冷月心,下期再见。