Redux Middleware中间件源码 分析

做者从2016年开始接触 React+Redux,经过阅读Redux源码,了解了其实现原理。git

Redux代码量很少,结构也很清晰,函数式编程思想贯穿着整个Redux源码,如纯函数,高阶函数,Curry,Compose。github

本文首先会介绍函数式编程的思想,再逐步介绍Redux中间件的实现。编程

看完本文,但愿能够帮助你了解中间件实现的原理。redux

1) 基本概念

Redux是可预测的状态管理框架。它很好的解决多交互,多数据源的诉求。闭包

Redux设计理念有三个原则: 1. 单一数据源 2. State只读 3. 使用纯函数变动state值。app

基本概念 原则 解释
Store (1) 单一数据源 (2) State只读 Store能够看作是数据存储的一个容器。在这个容器里面,只会维护惟一的一个State Tree。

Store会给定4种基础操做方法:dispatch(action), getState(), replaceReducer(nextReducer), subscribe(listener)框架

根据单一数据源原则,全部数据会经过store.getState()方法调用获取。dom

根据State只读原则,数据变动会经过store,dispatch(action)方法。koa

Action (3) 使用纯函数变动state值 Action能够理解为变动数据的信息载体。type是变动数据的惟一标志,payload是用来携带须要变动的数据。

格式为:const action = { type: 'xxx', payload: 'yyy' };异步

Reducer (3) 使用纯函数变动state值 Reducer是个纯函数。负责根据获取action.type的内容,计算state数值。

reducer: prevState => action => newState。

正常的一个同步数据流为:view层触发actionCreator,actionCreator经过store.dispatch(action)方法, 变动reducer。

可是面对多种多样的业务场景,同步数据流方式显然没法知足。对于改变reducer的异步数据操做,就须要用到中间件的概念。如图所示。

2) 函数式编程

函数式编程贯穿着Redux的核心。这里会简单介绍几个基本概念。若是你已经了解了函数式编程的核心技术,例如 高阶函数,compose, currying,递归,能够直接绕过这里。

我简单理解的函数式编程思想是: 经过函数的拆解,抽象,组合的方式去编程。复杂问题能够拆解成小粒度函数,最终利用组合函数的调用达成目的。

2.1) 高阶函数

Higher order functions can take functions as parameters and return functions as return values.

接受函数做为参数传入,并能返回封装后函数。

2.2) Compose

Composes functions from right to left.

组合函数,将函数串联起来执行。就像domino同样,推倒第一个函数,其余函数也跟着执行。

首先咱们看一个简单的例子。

// 实现公式: f(x) = (x + 100) * 2 - 100
const add = a => a + 100;
const multiple = m => m * 2;
const subtract = s => s - 100;
 
// 深度嵌套函数模式 deeply nested function,将全部函数串联执行起来。
subtract(multiple(add(200)));
复制代码

上述例子执行结果为:500

compose 实际上是经过reduce()方法,实现将全部函数的串联。不直接使用深度嵌套函数模式,加强了代码可读性。不要把它想的很难。

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)))
}
复制代码
compose(subtract, multiple, add)(200);复制代码

2.3) Currying

Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument

翻译过来是:把接受多个参数 的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数且返回结果的新函数的技术。

直接撸代码解释

// 实现公式: f(x, y, z) = (x + 100) * y - z;
const fn = (x, y, z) => (x + 100) * y - z;
fn(200, 2, 100);
  
// Curring实现 使用一层层包裹的单参匿名函数,来实现多参数函数的方法
const fn = x => y => z => (x + 100) * y - z;
fn(200)(2)(100);复制代码

*Currying只容许接受单参数。

3) Redux applyMiddleware.js

Redux中reducer更关注的是数据逻辑转化,因此Redux中间件是为了加强dispatch方法出现的。如咱们上面图,所描述的流程。中间件调用链,会在dispatch(action)方法以前调用。

因此Redux中间件实现核心目标是:改造dispatch方法。

redux对中间件的实现,代码是很精简。总体都不超过20行。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []
 
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
 
    return {
      ...store,
      dispatch
    }
  }
}
复制代码

接下来,一步步的解析Redux在中间件实现的过程。

applyMiddleware.js 方法有三个主要的步骤,以下:

  1. 将全部的中间件组合在一块儿, 并保证最后一个执行的是dispatch(action)方法。
  2. 像Koa全部中间件对ctx的调用同样。保证全部的中间件都能访问到Store。
  3. 最后将含有中间件调用链的新dispatch方法,合并到Store中。
  4. redux对中间件的定义格式为:mid1 = store => next => action => { next(action) };

看到这里,你可能有这么几个疑问?

  1. 如何将全部的middleware串联执行在一块儿?并能够保证最后一个执行的是dispatch方法?
  2. 如何让全部的中间件均可以访问到Store?
  3. 由于新造成的dispatch方法,为含有中间件调用链的方法结合。中间件若是调用dispatch,岂不是会死循环在调用链中?
  4. 为何将中间件格式定义为 mid1 = store => next => action => { next(action) } ?

为了解决这4个疑问,下面将针对相应问题,逐步解析。

3.1) 中间件串联

疑问:

  1. 如何将全部的middleware串联执行在一块儿?并能够保证最后一个执行的是dispatch(action)方法?

解决思路:

  1. 深度嵌套函数 / compose组合函数方法,将全部的中间件串联起来。
  2. 封装最后一个函数做为dispatch(action)方法。
const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
  1. compose(...)将全部中间件串联
  2. 定义final做为最后执行dispatch的函数
*/
compose(final, middleware2, middleware1)(action)
复制代码

3.2) 中间件可访问Store

疑问:

  1. 如何让全部的中间件均可以访问到Store?

能够参考咱们对Koa2中间件的定义 const koaMiddleware = async (ctx, next) => { };

解决思路:

  • 给每个middleware传递Store, 保证每个中间件访问到的都是一致的。
const middleware1 = (store, action) => action;
const middleware2 = (store, action) => action;
const final = (store, action) => store.dispatch(action);
复制代码

若是咱们想使用compose方法,将全部中间件串联起来,那就必须传递单一参数。

根据上面函数式编程讲到的currying方法,对每一个中间件柯里化处理。

// 柯里化处理参数
const middleware1 = store => action => action;
const middleware2 = store => action => action;
const final = store => action => store.dispatch(action);
 
// 将store保存在各个函数中 -> 循环执行处理。
const chain = [final, middleware2, middleware1].map(midItem => midItem(store));
compose(...chain)(action);
复制代码

经过循环处理,将store内容,传递给全部中间件。这里就体现了currying的做用,延迟计算和参数复用。

3.3) 中间件调用新dispatch方法死循环

疑问:

  1. 由于新造成的dispatch方法,为含有中间件调用链的方法结合。中间件若是调用dispatch,岂不是会死循环在调用链中?
new_dispatch = compose(...chain)(store.dispatch);   

new_store = { ...store, dispatch: new_dispatch };
复制代码

根据源码的解析,新和成new_dispatch是带有中间件调用链的新函数,并非原来使用的store.dispatch方法。

若是根据3.2) 例子使用的方式传入store, const chain = [final, middleware2, middleware1].map(midItem => midItem(store));

此时保存在各个中间件中的store.dispatch为已组合中间件dispatch方法,中间件若是调用dispatch方法,会发生死循环在调用链中。

根据上述文字的描述,右图是死循环的说明。

解决思路:

  1. 给定全部中间件的dispatch方法为原生store.dispatch方法,不是新和成的dispatch方法。
// 这就是为何在给全部middleware,共享Store的时候,会从新定义一遍getState和dispatch方法。

const middlewareAPI = {
  getState: store.getState,
  dispatch: (action) => dispatch(action)
}

chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
复制代码

3.4) 保证中间件不断裂

疑问:

  1. 为何将中间件格式定义为 mid1 = store => next => action => { next(action) } ?

上述例子有提到每次都会返回action给下一个中间件,例如 const middleware1 = store => action => action;

如何保证中间件不会由于没有传递action而断裂?

这里必须说明的是:Koa中间件能够经过调用await next()方法,继续执行下一个中间件,也能够中断当前执行,好比 ctx.response.body = ‘xxxx’ (直接中断下面中间件的执行)。

通常状况下,Redux不容许调用链中断,由于咱们最终须要改变state内容。(* 好比redux-thunk使用有意截断的除外)。

解决思路:

  1. 若是能够保证,上一个中间件都有下一个中间件的注册,相似Koa对下一个中间件调用方式next(),不就能够保证了中间件不会断裂。
// 柯里化处理参数
const middleware1 = store => next => action => { log(1); next(action)};
const middleware2 = store => next => action => { log(2); next(action)};
 
// 中间件串联
const chain = [middleware1, middleware2 ].map(midItem => midItem({
  dispatch: (action) => store.dispatch(action)}));
 
// compose(...chain)会造成一个调用链, next指代下一个函数的注册, 若是执行到了最后next就是原生的store.dispatch方法
dispatch = compose(...chain)(store.dispatch);
复制代码

4) 总结

Redux applyMiddleware.js机制的核心在于,函数式编程的compose组合函数,需将全部的中间件串联起来。

为了配合compose对单参函数的使用,对每一个中间件采用currying的设计。同时,利用闭包原理作到每一个中间件共享Store。

另外,Redux / React应用函数式编程思想设计,实际上是经过组合和抽象来减低软件管理复杂度。

简单写了个学习例子 参考 https://github.com/Linjiayu6/learn-redux-code, 若是有帮助到你,点个赞 咩~

简历请投递至邮箱linjiayu@meituan.com

相关文章
相关标签/搜索