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部分。

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;

​ 再会

情如风雪无常,

却是一动既殇。

感谢你这么好看还来阅读我的文章,

我是冷月心,下期再见。