本篇文章主要介绍Redux背后的架构思想和中间件模型
Redux 背后的Flux 架构思想
看源码首先得明白,Redux背后的架构思想,为什么他和React那么的搭配,我们可以认为 Redux 是 Flux 的一种实现形式(虽然它并不严格遵循 Flux 的设定),理解 Flux 将帮助你更好地从抽象层面把握 Redux。
首先flux架构将一个应用分成4个部分:
- view: 视图层
- action(动作):视图层发出的动作如: mouseclick
- dispatcher (派发器) :主要是用来接受actions,执行回调函数
- store(数据层):用来存放应用的状态,一旦发生变动就要提醒 view 视图层更新
从图中可以看出Flux最大的特点就是 ———— 单向数据流
- 用户访问view
- view发出用户的action
- dispatch 收到action, 则要求store 就行数据更新
- store 更新后 发出一个改变事件
- 接收到change视图更新
读到这里肯定就有人问? 就因为他这样所以我们要采用这种方式,因为前端项目有大多数是mvc 或者是mvvm架构。这种架构有什么缺点,有一个很大的缺点就是 当业务复杂度变得越来越高的时候,因为允许view 层和model层 直接传递。因为model可能不止对应一个view 层,就会出现下图这样的情况:
从图上看数据流这是真的混乱,如果项目中出现bug就很难定位到到底是哪一步出现问题,所以Flux 的核心是单向数据流, 因为视图更新就是从store通知视图更新。
Redux的架构其实是和Flux是非常像的:理解了Flux,自然理解了Redux, 毕竟整体的架构思想是非常像的。废话不多说吧,直接看图:
我给大家手动模拟下整个流程,用户点击鼠标发出一个action,经过Actions的处理,Actions 其实就是返回一个对象, 对象里面包含type 和所需要的数据 ,数据传到了Reducer 本质是一个纯函数,输入什么,输出什么,不做任何逻辑的运算。返回一个新的State之后,传递到数据中心Store,由Store通知视图更新。整个流程就结束了。那么Redux到底是怎么做到的,带着好奇心,我和大家一起撸一遍源码,看他里面到底有什么奥妙?
Redux源码解析
Redux 源码的目录结构十分简单, types 主要存放的是ts 定义的一些类型, utils主要是一些通用方法,没什么涉及关键流程的,所以接下来我就主要分析 applyMiddleware 、 combinReducers、compose、 createStrore这三个ts 文件
createStore作为Redux的开始 我们先分析这个文件
CreateStore
// 引入 redux
import { createStore } from 'redux'
// 创建 store
const store = createStore(
reducer,
initial_state,
applyMiddleware(middleware1, middleware2, ...)
);
从图中createStore 接受3个参数
- 第一个参数就是一个reducer 一个纯函数 ,由我们自己定义
- 第二个参数 初始化的数状态
- 第三个参数 其实就是制定中间件 在源码中就是enhancer 增强store
从拿到入参到返回出 store 的过程中,到底都发生了什么呢?这里我为你提取了 createStore 中主体逻辑的源码(解析在注释里):
这段代码主要是做一些类型判断, 和一些写法兼容没什么。继续往下看, 下面是一些初始状态的赋值:
肯定有部分同学对这里的快照和浅拷贝确保不同的引用有疑问? 我这里先卖个关子,等整个流程走完后面重点分析why?? 接下来就进入我们经常用的getState函数了。
getState:
我草就这么几行代码,十分的简单,源码也就那样嘛,easy easy! 继续往下看
subscribe:
这里订阅的时候浅拷贝了一下,卸载的时候也浅拷贝,用的都是nextListeners, 还记得我们有个currentListeners吧, 难道说这个一点用都没有嘛。 我们接着往下看。
dispatch:
dispatch 的时候: 又将next 重新复制给 current 然后执行每个listenr. 看到这里我想你应该明白reducer 中 我dispatch ? 或者做一些subscribe 做一些脏操作,redux 源码中为了防止这种 就是设置 isDispatching 这个 变量来控制。
所以dispacth一个action? Redux 帮我们做了啥事, 就很简单2件事
- oldState 经过reducer 产生了newState, 更新了store数据中心
- 触发订阅
整个Redux 的工作流,到这里其实已经结束了, 但是我们还有一个疑问就是 subscribe 为啥都是nextListeners 然后在dispatch 的 又把值重新赋给currentListeners? 这是为什么呢??
答案就是:为了保证触发订阅的稳定性
这句话怎么理解呢我举一个例子:
// 定义监听函数a
function listenera() {
}
// 订阅 a,并获取 a 的解绑函数
const unSubscribea = store.subscribe(listenera)
// 定义监听函数 b
function listenerb() {
// 在 b 中解绑 a
unSubscribea()
}
// 定义监听函数 c
function listenerc() {
}
// 订阅 b
store.subscribe(listenerb)
// 订阅 c
store.subscribe(listenerc)
从上文我可以得知当前的currentListeners:
[listenera, listenerb, listenerc]
但是比较特殊的是listenb 其实卸载 listena的, OK如果我们不浅拷贝一下, 那么触发订阅的时候数组遍历到 i = 2 的时候其实数组是undefined , 这样引发报错, 因为我们在 订阅前和卸载订阅都浅拷贝一下,nextListeners数据随便怎么变, 只要保证currentListener 稳定 就好了。
本次dispacth完之后,下一次dispacth 假设没有新增订阅, 数据关系又重新赋值。
listeners =( currentListeners = nextListeners)
你仔细回想一下这个变化,是不是所有就理解的通了, 这也是为什么Redux 订阅稳定的原因了啦。设计真的是十分的巧妙哇,读到现在发现源码其实并没有想象的辣么难? 细节处理满分哇。 接下来就是分析Redux的中间件模型。
Redux中的中间件思想
要想理解redux 的中间件思想, 我们先 看下compose这个文件做了什么, 这个文件其实做的事情十分简单。主要是用到函数式编程中的组合概念, 将多个函数调用组合成一个函数调用。
其实主要是reduce 的这个api,为了方面大家理解, 我就简单手写下数组reduce 的实现:
ok 这东西其实就是个累加器按照某种方式呗。我们接下来就直接进入compose函数话不多说直接看代码:
// 参数是函数数组
export default function compose(...funcs: Function[]) {
// 处理边界情况
if (funcs.length === 0) { return <T>(arg: T) => arg } // 数量为1 就没有组合的必要了 if (funcs.length === 1) { return funcs[0] } // 主要是下面这一行代码, 你细品👌 return funcs.reduce((a, b) => (...args: any) => a(b(...args)))}
OK, 我现在还是带大家分析下这行代码,同样是我举例子说明: 假设我们有这个3个函数:
funcs = [fa, fb, fc]
由于没有出初始值累加器的就是 fa 经过一次遍历后, accumulateur 变为下面的样子:
let m = (...args) => fa(fb(...args))
在经过一次遍历后, 此时b = fc 此时accumulateur 变为下面的样子:
**(...args) => m(fc(...args))**
我们将fc(...args) 看做一个整体带入上面m的函数 所以就变成了下面的样子
(...args)=> fa(fb(fc(...args)))
OK到这里 就大工告成了,fa, fb, fc 我们可以想象成 redux 的3个中间件, 按照顺序传进去,
当这个函数被调用的时候也会按照 fa, fb, fc 顺序调用。前面铺垫结束就直接开始分析? compose 是如何 和Redux 做结合的。
applyMiddleWare
我先把整体结构分析下, 和函数参数分下:
// applyMiddlerware 会使用“...”运算符将入参收敛为一个数组
export default function applyMiddleware(...middlewares) {
// 它返回的是一个接收 createStore 为入参的函数
return createStore => (...args) => {
......
}
}
createStore 就是 上面我们分析过的 创建数据中心Store ,而 args 主要是有两个, 还是createStore 两个约定入参 一个是reducer, 一个是 initState。
enhance-dispatch
接下来就是比较核心的, 改写dispacth, 为什么要改写dispatch, 还是举个例子说明。 不知道大家还记不记得 dispacth 接受的action 只能是对象, 如果不是对象的话会直接报类型错误。如图:
OK社区比较有名的redux-thunk 中间件, dispatch 可以接受一个函数, 难道是她绕过了我们的检查,那肯定不是,计算机肯定不会骗人的。 那只有一个原因。
应用了中间件的dispatch 和 没有用中间的dispatch 肯定是不等的,
dispatch = enhancer(dispacth) 肯定是增强的至于怎么个增强法 继续往下看。
const store = createStore(...args)
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
chain = middlewares.map(middleware => middleware(middlewareAPI)) // 绑定 {dispatch和getState}
dispatch = compose(...chain)(store.dispatch)
从代码中可以看到的第一步 其实就是将 getState 和 dispatch 作为中间件中的闭包使用, 有人会这里提问,这里为什么是匿名函数 ? 而不是store.dispatch ? 这里你可以自行先🤔下, 后面解答。
可能很多人看到这里还是很懵, 还是举个例子说明, 还记得上面的 fa , fb, fc 接下来 我就把他们展开。
function fa(store) {
return function(next) {
return function(action) {
console.log('A middleware1 开始');
next(action)
console.log('B middleware1 结束');
};
};
}
function fb(store) {
return function(next) {
return function(action) {
console.log('C middleware2 开始');
next(action)
console.log('D middleware2 结束');
};
};
}
function fc(store) {
return function(next) {
return function(action) {
console.log('E middleware3 开始');
next(action)
console.log('F middleware3 结束');
};
};
}
ok 我们一步一步分析, 看到底做了什么。
chain = middlewares.map(middleware => middleware(middlewareAPI))
很显然 我们 的 middlewares = [fa, fb, fc] map 之后返回一个新的 chain 这时候的chain 应该是下面 这样子的:
chain = [ (next)=>(action)=>{...}, (next) => (action) => {...}, (next) => (action) => {...} ]
只不过chain中的每一个元素 都有getSate,dispatch 的闭包而已。
ok 继续往下走就到了compose 函数 还记得上面我们compose(fa, fb, fc ) 返回值是什么?
(...args)=> fa(fb(fc(...args)))
对的就是这个东西, 我们这里的chain 也是一样的, 所以这里的 dispatch 就变成了增强的dispatch 我们一起看下
dispatch = fa(fb(fc(store.dispacth)))
看到这里有人就问? 每一个中间件的next 和 原先的store.dispacth 有什么不同 ? 这和洋葱模型有什么关系?
那我就带你一步一步分析, 这里的dispatch 就是指的是当前的 fa(fb(fc(store.dispatch))) , 我们可以直接函数调用来分析,
fa 的参数 是 fb(fc(store.dispatch)) , 由于依赖fb, 所以调用fb, 然后发现fb 依赖的参数是 fc(store.dispacth), 紧接着又开始调用fc, ok 到这里终于结束了, 终于没有依赖了。 所以从上面的过程我们可以得到 next 其实是他上个中间件的副作用, 最后一个中间件的next 就是****store.dispatch。
副作用: 每个中间件的中的 (action )=> {...}
我用流程图表示整个洋葱模型流程:
当我调用dispatch 的时候, 先打印 E, 然后发现 next 是副作用fb, 然后调用副作用fb, 打印c,发现 next 竟然是 副作用fc ,再去调用fc, 打印A, next 这时候就是 store.dispacth, 调用结束, 打印 b, 然后打印D, 最后打印 F 。 这样的一系列操作是不是有点像洋葱模型, 如果我可以一层一层拨开你的心, 哈哈哈哈, 太骚了, 好了回归主题。还记得我之前提出的问题?
1.为什么dispacth 是匿名函数?
2. 为什么dispacth 一个action后, 还是返回action
问题1: 为什么dispatch 是是一个匿名函数 , 因为有的中间件原理的实现,并不会 next(action), 这时候需要肯定是增强的dispacth, redux-thunk 的执行原理, 就是当你传递一个函数, 直接调用这个函数,并把dispatch 的 权限 交给你自己处理。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
// 这个的dispatch 其实是增强的dispatch,
// 如果用store.dispatch如果还有其他中间件就丢失了 return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
这里看下源码:
// 这个其实dispatch 其实 是个闭包let dispatch: Dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' )}const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args)}const chain = middlewares.map(middleware => middleware(middlewareAPI))// compose 完之后将闭包更新成增强的dispatchdispatch = compose<typeof dispatch>(...chain)(store.dispatch)
所以Redux 严格 意义上 并不算是洋葱模型, 他的洋葱模型是建立在你的每个中间件,都要next(action); 如果不 next(action) 其实就破坏了洋葱模型。
问题2: dispatch (action) 返回action, 就是为了方便下一个中间件的处理。
到这里,Redux源码主要流程已经全部分析完毕, 文章上哪里有不对的,欢迎指正交流。
参考文献