深入浅出redux-middleware

多数redux初学者都会使用redux-thunk这个中间件来处理异步请求(比如我)

本来写这篇文章只是想写写redux-thunk,然后发现还不够,就顺便把middleware给过了一遍。

为什么叫thunk?

thunk是一种包裹一些稍后执行的表达式的函数。

redux-thunk源码

所有的代码就只有15行,我说的是真的。。 redux-thunk

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

代码很精简,但是功能强大,所以非常有必要去了解一下。

redux-middleware是个啥

image

上图描述了一个redux中简单的同步数据流动的场景,点击button后,dispatch一个action,reducer 收到 action 后,更新state后告诉UI,帮我重新渲染一下。

redux-middleware就是让我们在dispatch action之后,在action到达reducer之前,再做一点微小的工作,比如打印一下日志什么的。试想一下,如果不用middleware要怎么做,最navie的方法就是每次在调用store.dispatch(action)的时候,都console.log一下actionnext State

store.dispatch(addTodo('Use Redux'));
  • naive的方法,唉,每次都写上吧
const action = addTodo('Use Redux');

console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());
  • 既然每次都差不多,那封装一下吧
function dispatchAndLog(store, action) {
  console.log('dispatching', action);
  store.dispatch(action);
  console.log('next state', store.getState());
}
  • 现在问题来了,每次dispatch的时候都要import这个函数进来,有点麻烦是不是,那怎么办呢?

既然dispatch是逃不走的,那就在这里动下手脚,reduxstore就是一个有几种方法的对象,那我们就简单修改一下dispatch方法。

const next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action); // 之前是 `dispatch(action)`
  console.log('next state', store.getState());
}

这样一来我们无论在哪里dispatch一个action,都能实现想要的功能了,这就是中间件的雏形。

image
  • 现在问题又来了,大佬要让你加一个功能咋办?比如要异常处理一下

接下来就是怎么加入多个中间件了。

function patchStoreToAddLogging(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function patchStoreToAddCrashReporting(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action)
    } catch (err) {
      console.error('Caught an exception!', err)
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState()
        }
      })
      throw err
    }
  }
}

patchStoreToAddLoggingpatchStoreToAddCrashReportingdispatch进行了重写,依次调用这个两个函数之后,就能实现打印日志和异常处理的功能。

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
  • 之前我们写了一个函数来代替了store.dispatch。如果直接返回一个新的dispatch函数呢?
function logger(store) {
  const next = store.dispatch

  // 之前:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

这样写的话我们就需要让store.dispatch等于这个新返回的函数,再另外写一个函数,把上面两个middleware连接起来。

function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()

  // Transform dispatch function with each middleware.
  middlewares.forEach(middleware => (store.dispatch = middleware(store)))
}

middleware(store)会返回一个新的函数,赋值给store.dispatch,下一个middleware就能拿到一个的结果。

接下来就可以这样使用了,是不是优雅了一些。

applyMiddlewareByMonkeypatching(store, [logger, crashReporter])

我们为什么还要重写dispatch呢?当然啦,因为这样每个中间件都可以访问或者调用之前封装过的store.dispatch,不然下一个middleware就拿不到最新的dispatch了。

function logger(store) {
  // Must point to the function returned by the previous middleware:
  const next = store.dispatch

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

连接middleware是很有必要的。

但是还有别的办法,通过柯里化的形式,middlewaredispatch作为一个叫next的参数传入,而不是直接从store里拿。

function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}

柯里化就是把接受多个参数的函数编程接受一个单一参数(注意是单一参数)的函数,并返回接受余下的参数且返回一个新的函数。

举个例子:

const sum = (a, b, c) => a + b + c;

// Curring
const sum = a => b => c => a + b + c;

ES6的箭头函数,看起来更加舒服。

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

接下来我们就可以写一个applyMiddleware了。

// 注意:这是简单的实现
function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()
  let dispatch = store.dispatch
  middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
  return Object.assign({}, store, { dispatch })
}

上面的方法,不用立刻对store.dispatch赋值,而是赋值给一个变量dispatch,通过dispatch = middleware(store)(dispatch)来连接。

现在来看下reduxapplyMiddleware是怎么实现的?

applyMiddleware

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */
 
 // 就是把上一个函数的返回结果作为下一个函数的参数传入, compose(f, g, h)和(...args) => f(g(h(...args)))等效

export default 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最后返回的也是一个函数,接收一个参数args

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    
    // 确保每个`middleware`都能访问到`getState`和`dispatch`
    
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // wrapDispatchToAddLogging(store)
    dispatch = compose(...chain)(store.dispatch)
    
    // wrapCrashReport(wrapDispatchToAddLogging(store.dispatch))

    return {
      ...store,
      dispatch
    }
  }
}

image

借用一下大佬的图, google搜索redux-middleware第一张

到这里我们来看一下applyMiddleware是怎样在createStore中实现的。

export default function createStore(reducer, preloadedState, enhancer){
  ...
}

createStore接受三个参数:reducer, initialState, enhancerenhancer就是传入的applyMiddleware函数。

createStore-enhancer #53

//在enhancer有效的情况下,createStore会返回enhancer(createStore)(reducer, preloadedState)。
return enhancer(createStore)(reducer, preloadedState)

我们来看下刚刚的applyMiddleware,是不是一下子明白了呢。

return createStore => (...args) => {
    // ....
}

到这里应该就很容易理解redux-thunk的实现了,他做的事情就是判断action 类型是否是函数,如果是就执行action,否则就继续传递action到下个 middleware

参考文档:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容

  • http://gaearon.github.io/redux/index.html ,文档在 http://rac...
    jacobbubu阅读 79,737评论 35 198
  • 一、什么情况需要redux? 1、用户的使用方式复杂 2、不同身份的用户有不同的使用方式(比如普通用户和管...
    初晨的笔记阅读 1,947评论 0 11
  • 前言 本文 有配套视频,可以酌情观看。 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我讨论。 文中所有内...
    珍此良辰阅读 11,809评论 23 111
  • 本文将开始详细分析如何搭建一个React应用架构。 一. 前言 现在已经有很多脚手架工具,如create-reac...
    字节跳动技术团队阅读 4,188评论 1 23
  • Redux API 总览 浅谈redux 中间件的原理 原文 在 Redux 的源码目录 src/,我们可以看到如...
    谷子多阅读 459评论 0 1