Koa2和Redux中间件源码研究

一、Koa2中间件源码分析

在Koa2中,中间件被存放在一个数组中。 使用koa中,最常见的就是app.use(fn),use函数部分源码如下所示。首先中间件必须是个函数。若是generator函数,则需要进行转化。最后把该中间件推入middelaware数组中。

constructor() {
    this.middleware = [];
}
use(fn) {
    this.middleware.push(fn);
}
复制代码

当调用app.listen函数时,实际上是创建了一个原生http服务器,并执行了Koa自身的callback方法。

listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
}
复制代码

callback函数中compose是将中间件封装成一个迭代器,按照middleware数组中顺序执行下去,实现了面向切面编程。handleRequest函数则是将req和res封装成ctx,并执行中间件。

callback() {
   const fn = compose(this.middleware);
   const handleRequest = (req, res) => {
     const ctx = this.createContext(req, res);
     return this.handleRequest(ctx, fn);
   };
   return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
   ...
   return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码

compose用来返回一个迭代器函数fnMiddleware,该函数接受两个参数,context和next。因为调用时只向fnMiddleware传入了第一个参数context,所以next参数的值一直是undefined。

首先定义了index,dispatch传入的参数 i 作为每个中间件的调用标志,每次中间件调用next函数都会让index+1,若是中间件多次调用next,则会使index大于等于该标志,就会报错。

然后依次读取中间件数组中的中间件,并将context和封装了调用标志i的dispatch函数传给中间件,就实现了中间件的过程。
当读取完中间件数组后,即i === middleware.length时,将值为是undefined的next传给fn并结束迭代。dispatch函数所有的返回值都是Promise函数,这样就可以实现异步编程。

function compose (middleware) {
  return function (context, next) {
    // last called middleware #
    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
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
复制代码

二、Redux中间件源码分析

在Redux中需要引入中间件的话,需引入applyMiddleware函数并将其作为参数传给createStore。下方enhancer就是就是applyMiddleware的返回值。

export default function createStore(reducer, preloadedState, enhancer) {
    ...
    return enhancer(createStore)(reducer, preloadedState)
}
复制代码

applyMiddleware接受一个中间件数组为参数。

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)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
复制代码

其中compose用Array.prototype.reduce将中间件数组封装成一个层层包裹的函数。

function compose(...funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
复制代码

若本来的中间件数组是

[a, b, c]

扫描二维码关注公众号,回复: 3163768 查看本文章

经过compose函数封装后,就成了

(...arg) => a(b(c(...arg)))

    dispatch = compose(...chain)(store.dispatch)
复制代码

经过这个过程后,每次调用dispatch就会在中间件中进行遍历。每个中间件都需要调用next(action)来保证中间件链都能被执行。

Redux中中间件的写法一般为

    const middleware = store => next => action => {...}
复制代码

根据上面的分析,store参数就是middlewareAPI,但是其中的dispatch并不是真正的dispatch,这是为了防止在中间件中调用store.dispatch而导致重新遍历整个中间件链。next是下一个中间件,需要传递action,直到最后一个中间件时,next即是原始的store.dispatch。

三、Koa2和Redux中间件比较

在两者中都出现了compose函数。
Koa2中的compose函数实现原理是用dispatch函数自身迭代,Redux中的compose实现原理是用了数组的reduce方法。两者都按照数组顺序执行中间件,并且先执行的中间件可以获取后执行的中间件的状态(store的状态或者context的状态),实现了面向切面编程。

猜你喜欢

转载自juejin.im/post/5b94b86c5188255c6a041c39