从源码理解Redux和Koa2的中间件机制

Redux和Koa的中间件机制相关源码都很精简。

正文我将直接引用部分源码,并加以注释来帮助我们更清晰的理解中间件机制。

Reudx

redux的中间件机制在源码中主要涉及两个模块

内部的compose组合函数
'redux/src/compose.js'

//compose组合函数,接收一组函数参数返回一个组合函数
//需要提前注意的一点是,funcs数组内的函数基本上(被注入了api)就是我们在未来添加的中间件如logger,thunk`等
export default function compose(...funcs) {
//为了保证输出的一致性,始终返回一个函数
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
 //这一步可能有些抽象,但代码极其精致,通过归约函数处理数组,最终返回一个逐层自调用的组合函数。
 //例: compose(f, g, h) 返回 (...args) => f(g(h(...args))).
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

//或许是版本更新的缘故,相比之前看到过的compose要精简了很多,尤其是在最终的规约函数处理上,高大上了不少。
//由原本的reduce来依次执行中间件进化为函数自调用,更加的【函数式】。。下面顺便贴出可能是旧的compose函数,大家自行对比。

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
复制代码
我们添加中间件时最常用到的redux提供的applyMiddleware函数
'redux/src/applyMiddleware.js'

//可以看到compose函数被作为?引入了
import compose from './compose'
//暴露给开发者,用来传入中间件
export default function applyMiddleware(...middlewares) {
    //createStore函数的控制权将被转让给applyMiddleware,由于本篇主要谈中间件,就不扩展来解释了
  return createStore => (...args) => {
//------------------------------------------------------------------非中间件相关,一些上下文环境的代码-----------------------
    //初始化store,此处的...args实际为reducer, preloadedState(可选)
    const store = createStore(...args)
    //声明一个零时的dispatch函数,注意这里的let,它将在构建完毕后被替换
    let dispatch = () => {
     throw new Error('dispatch不允许在构建中间件的时候被调用,其实主要是为了防止用户自定义的中间件在初始化的时候调用dispatch。
                        在下文的示例中可以看到, 并且普通的同步的中间件一般是用不到dispatch的')
    }
    //提供给中间件函数的api,可以看到dispatch函数在这里通过函数来'动态的调用当前环境下的dispatch'
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    //为中间件注入api
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
//------------------------------------------------------------------------------------------------------------------------
    
    '关键'
    //列出上文的例子可能比较直观: 调用compose(f, g, h)返回(...args) => f(g(h(...args))).
    //1.调用compose函数,返回一个由多个/一个中间件构成的组合函数
    //2.将store.dispatch作为参数传入组合函数中用来返回一个新的/包装过的dispatch函数
    //'注意:这部分需要联系下文中的中间件源码来对照着进行理解,所以让我们暂时把这里加入脑内缓存'
    dispatch = compose(...chain)(store.dispatch)
    
    //返回一个store对象,在添加了中间件的情况下,我们实际最终获取的store就是从这里拿到的。
    return {
      ...store,
      dispatch
    }
  }
}
复制代码
第三方中间件

本来不想写这么长来着,但希望更多大家能够更简单的理解,就多贴了些源码,毕竟代码远比文字更好理解,下面我用logger和thunk的源码(简化)来做承接上文的简要分析。

'redux-logger'
//由于logger源码看起来好像有点复杂(懒得看),我就简单实现了...不严谨请轻喷

通常来说redux的中间件主要分为两层。

//第一层,用于接受store提供的API,在传给构建中间件之前就会被调用。
const logger = ({getState}) => {
    // 第二层,利用了函数(currying)柯理化将计算/运行延迟,请让我用更多的注释来帮我们理清思路...
    // 还是先列出上文的例子比较直观【手动滑稽】:)
    // 例:compose(f, g, h)返回(...args) => f(g(h(...args))). 
    // 关联上文:dispatch = compose(...chain)(store.dispatch) 代入例子 ((dispatch) => f(g(h(dispatch))(store.dispatch)
    // 可以看到清晰看到,中间件被自右向左执行,store.dispatch作为参数被传入给最先执行(最右侧)的中间件
    // 中间件的第二层被执行,返回一个'接受action作为参数的函数',这个函数作为调用下一个(自己左侧)的中间件,依次执行至最左侧,最终返回的同样是一个'接受action的函数'
    // 最终我们调用的dispatch实际上就是这个被最终返回的函数
    // '我们的真实流程是 dispatch(包装过的) => 中间件1 => 中间件2 => dispatch(store提供的) => 中间件2 => 中间件1 => 赋值(如果有返回的话)'
    // 果然还是没有解释清楚,请抛开我的注释,多看几遍代码
    return next => action => {
        console.log('action', action)
        console.log('pre state', getState())
        //next实质就是下一个(右侧)中间件返回的闭包函数/当前中间件如果是最后一个或者唯一的,那么next就是store提供的dispatch
        //next(action)函数调用栈继续往下走,也就是调用下一个(右侧)中间件,nextVal会接受返回的结果
        const nextVal = next(action)
        console.log('next state', getState())
        //将结果返回给上一个中间件(左侧)或者是开发者(第一个中间件的情况下)
        return nextVal
    }
}


'redux-thunk'
//这个是官方的源码,异常精简,这个函数支持了dispatch的异步操作,让我们来看看如何实现的。
//这里就不复述上面的注释了,只解释下关于异步的支持。
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        //将dispatch函数的执行权限转移给开发者,我们通常在异步结束之后调用dispatch(此时是同步的)。
        //注意:在这里我们原本的中间件执行流程被中断,并重新以同步的模式执行了一遍,'所以redux-thunk在中间件中的位置将会对其余中间件造成影响,例如logger中间件被执行了两次什么的...'
        // 另一个要注意的是,这里的dispatch函数实际上是在构筑中间件后被包装的函数。
        return action(dispatch, getState, extraArgument);
    }
    //dispatch同步时,直接将控制权转让给下一个中间件。
    //dispatch异步时,在异步结束后调用的dispatch中,同样将控制权转让给下一个中间件。
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

复制代码
小结

最后让我们梳理总结一下Redux的中间件流程。

  1. 首先是提供一个compsoe函数用来生成一个由多个中间件构成的组合函数(存在自调用能力)
  2. 将store的API注入中间件
  3. 将store.dispath作为参数传递给一个由compose函数构成的组合函数,返回一个包装后的dispatch(也就是我们真实使用的dispatch)
  4. (或者是0.)构筑一个特定结构的的中间件,第一层用于注入API,第二层用来接受上一个中间件返回的一个接受action作为参数的函数,并且自身同样返回一个包含中间件具体操作的接受action作为参数的函数
  5. 由中间件提供的dispatch被调用,中间件被依次调用,如果遇到提供了异步支持,那么在异步情况下,dispatch会先按照普通流程调用,当遇到redux-thunk或者redux-promise等函数时,会以同步的形式重新调用当前dispatch(中间件也会被重新调用一遍)

下面丢张费了九牛二虎之力画的调用流程图...随意看看就好...

Koa2

感觉基本没多少朋友看到这里了吧...但我还是要写完。 同上,先贴源码让代码来告诉我们真相

在redux里,中间件是作为一个附加的功能存在的,但在koa里中间件是它最主要但机制。

koa的核心代码被分散在多个独立的库中,首先来看中间件机制核心的compose函数
'koa-compose'

//注意:'在函数中始终返回Promise,是由于koa2采用了async await语法糖形式'
//接受一个中间件数组
function compose (middleware) {
    返回一个处理函数,在Request请求的最后被调用,并传入请求的相关参数
  return function (context, next) {
    let index = -1
    //执行并返回第一个中间件
    return dispatch(0)
    
    一个接受一个数字参数,用来依次调用中间件
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      //在只有一个中间件的时候直接调用自身
      if (i === middleware.length) fn = next
      //中间件被执行完毕了,直接返回一个Promise
      if (!fn) return Promise.resolve()
      try {
        //将下一个中间件的函数调用包裹在next中,返回给当前的中间件
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        //监听错误,并抛出
        return Promise.reject(err)
      }
    }
  }
}

//相比较redux的中间件缺少了几分函数式的精致,但我依旧写不出类似精简的代码.jpg
复制代码
koa本体
'koa/lib/application.js'

'104-115行的use函数(简化)'

//这是koa暴露给我们的use函数,相信大多数同学都不陌生
  use(fn) {
    //非常明了,就是将中间件添加入middleware数组
    this.middleware.push(fn);
    return this;
  }

'125-136行的callback函数'
//callback函数将在koa.listen中被调用具体请自行查看源码

  callback() {
    //调用compsoe
    const fn = compose(this.middleware);
    if (!this.listeners('error').length) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      //Request时被调用
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }

复制代码
koa2小结

关于koa2的中间件机制我并没有解释多少,主要是由于相比redux中间件来说简明许多,另一个原因主要是懒,具体的执行流程图,实际上同样是洋葱形的,只是store.dispatch被换成了最后一个中间件而已。

本篇文章,虽然质量不行,大多注释偏口语化(专业词汇量不足),但还是希望能够对一些同学有所帮助。

临渊羡鱼不如退而结网

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值