带你从头到尾系统地撸一遍Redux源码

1,505 阅读11分钟

本篇文章主要介绍Redux背后的架构思想和中间件模型

Redux 背后的Flux 架构思想

看源码首先得明白,Redux背后的架构思想,为什么他和React那么的搭配,我们可以认为 Redux 是 Flux 的一种实现形式(虽然它并不严格遵循 Flux 的设定),理解 Flux 将帮助你更好地从抽象层面把握 Redux。

首先flux架构将一个应用分成4个部分:

  1. view:  视图层
  2. action(动作):视图层发出的动作如: mouseclick
  3. dispatcher (派发器) :主要是用来接受actions,执行回调函数
  4. store(数据层):用来存放应用的状态,一旦发生变动就要提醒 view 视图层更新

从图中可以看出Flux最大的特点就是 ———— 单向数据流

  1. 用户访问view 
  2. view发出用户的action
  3. dispatch 收到action, 则要求store 就行数据更新
  4. store 更新后 发出一个改变事件
  5. 接收到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件事

  1.  oldState 经过reducer 产生了newState, 更新了store数据中心
  2.  触发订阅

整个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源码主要流程已经全部分析完毕, 文章上哪里有不对的,欢迎指正交流。

参考文献

深入浅出搞定React - 修言

applyMiddleWare中间件解析