实现洋葱圈模型的三种方法

424 阅读2分钟

背景

洋葱圈模型是前端一个经典概念,起源于Koa,那么底层如何实现?介绍一下我看过的三种不同实现方法。

首先介绍一道面试题

let middleware = []
middleware.push((next) => {
    console.log(1)
    next()
    console.log(1.1)
})
middleware.push((next) => {
    console.log(2)
    next()
    console.log(2.1)
})
middleware.push((next) => {
    console.log(3)
    next()
    console.log(3.1)
})

let fn = compose(middleware)
fn()

期望输出结果当然是 1 2 3 3.1 2.1 1.1

分析一下题目,这里的compose起到了组合函数的作用,举个例子A、B、C三个函数经过 componse([A,B,C]),期望最后执行顺序变成A(B(C))

KOA

源码很简洁:github.com/koajs/compo…

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  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)
      }
    }
  }
}

使用了递归的思想,dispatch.bind(null, i + 1) 返回第i+1个函数,实现了 middleware[i](middleware[i+1])的效果,并一层一层递归。

如果用来解题的话,我会这么写

function compose(middlewares) {
    return function(){
        const dispatch = (index)=>{
          if(index === middlewares.length){
            return 
          }
          return middlewares[index](dispatch.bind(null,index+1))
        }
        return dispatch(0)
    }
}

Redux

Redux的插件系统也是洋葱圈模型,而与koa相比,插件写法不同。

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

这里是一个柯里化写法,结合applyMiddleware组装函数去理解。通过遍历middlewares,不断更新dispatch函数,最后返回一个dispatch。这样每次dispatch(action),就都会通过middlewares的处理。

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 })
}

具体可以参考 理解 Middleware

那么如果这道题修改一下

function M1(store) {
    return function(next) {
      return function() {
        console.log(1)
        next()
        console.log(1.1)
      };
    };
  }
  
  function M2(store) {
    return function(next) {
      return function() {
        console.log(2)
        next()
        console.log(2.1)
      };
    };
  }
  
  function M3(store) {
    return function(next) {
      return function() {
        console.log(3)
        next()
        console.log(3.1)
      };
    };
  }

compose可以这么写

function compose( middlewares) {
    middlewares = middlewares.slice()
    middlewares.reverse()
    let dispatch = ()=>{}
    middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
    return dispatch
 }

Taro

Taro网路请求的拦截器也实现了一个洋葱圈模型,具体可看Taro.addInterceptor(interceptor)

koa和redux的实现都是用一个compose函数进行组合,返回一个组合后的函数。taro则更像击鼓传花,在插件里去触发下一个插件,并将处理后的requestParams传给下一个插件。

首先看一下插件写法

  • 入参是一个chain对象,相当于ctx。
  • chain.proceed将requestParams传递给下一个插件。

export function timeoutInterceptor (chain) {
  const requestParams = chain.requestParams
  let p
  const res = new Promise((resolve, reject) => {
    let timeout = setTimeout(() => {
      timeout = null
      reject(new Error('网络链接超时,请稍后再试!'))
    }, (requestParams && requestParams.timeout) || 60000)

    p = chain.proceed(requestParams)
    p
      .then(res => {
        if (!timeout) return
        clearTimeout(timeout)
        resolve(res)
      })
      .catch(err => {
        timeout && clearTimeout(timeout)
        reject(err)
      })
  })
  if (!isUndefined(p) && isFunction(p.abort)) res.abort = p.abort

  return res
}

再看一下chain的实现,Chain对象的interceptors是一个插件列表,proceed()会根据index找到下一个插件。

import { isFunction } from '../utils'
export default class Chain {
  constructor (requestParams, interceptors, index) {
    this.index = index || 0
    this.requestParams = requestParams
    this.interceptors = interceptors || []
  }

  proceed (requestParams) {
    this.requestParams = requestParams
    if (this.index >= this.interceptors.length) {
      throw new Error('chain 参数错误, 请勿直接修改 request.chain')
    }
    const nextInterceptor = this._getNextInterceptor()
    const nextChain = this._getNextChain()
    const p = nextInterceptor(nextChain)
    const res = p.catch(err => Promise.reject(err))
    Object.keys(p).forEach(k => isFunction(p[k]) && (res[k] = p[k]))
    return res
  }

  _getNextInterceptor () {
    return this.interceptors[this.index]
  }

  _getNextChain () {
    return new Chain(this.requestParams, this.interceptors, this.index + 1)
  }
}

总结下来,三种不同的方式对应的是不同场景,了解下来还是蛮有趣的。