一, 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]
经过 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 的状态), 实现了面向切面编程.
来源: https://juejin.im/post/5b94b86c5188255c6a041c39