前言
无论是nodeJS框架koa、egg,还是小程序云开发框架tcb-router,在处理中间件时都采用了洋葱模型。
在这里肯定还有很多朋友不知道什么是中间件,我这引用一段很通俗的话:
中间件相当于太监,内部的核心业务相当于皇上,大臣在上奏时,不会直接传输给皇上,要通过太监呈递、安全处理了才递给皇上。
图中请求会通过中间件层层递进到内部,再从内部到外部传输,有点类似于前端的事件捕获,先从外到内,在从内到外的机制。
看到这里你可能还不太懂怎么去实现这样一个模型?这个模型又有什么样的用处?那么你就容我细细道来,以小白的视角窥探事物的本质。
什么是洋葱模型
在讲什么是洋葱模型之前,我们先来一道题。
function fn1(next) {
console.log(1)
next()
console.log(6)
}
function fn2(next) {
console.log(2)
next()
console.log(5)
}
function fn3(next) {
console.log(3)
console.log(4)
}
/**
* 设计一个compose函数, 在执行这个函数的时候,去调用传入的函数参数,并且输出 1、2、3、4、5、6
*/
compose([fn1,fn2,fn3])
function compose(fnList){}
流程图分析
由图不难看出:
next指向的是下一个函数的地址,执行next函数,就是在函数内部执行下一个索引的函数fn3函数因为没有下一个函数了,next就没有指向,所有就没有next- 每个函数被
next分成了三份 - 以上实现模式的算法,使用递归调用,又借助了栈的思想(先进后出)
代码实现分析
代码中最复杂的就是next函数的指向,只要保证 next指向下一个函数,那就很容易实现洋葱模型
//compose([fn1, fn2, fn3])
function compose(fnList) {
return dispatch(0)
/**
* 执行fnList中的函数,生成next参数,并且当作形参传入要执行的函数
* @param {number} i 当前要执行那个函数的索引
*/
function dispatch(i) {
let fn = fnList[i] //要执行那个函数
let nextFnI = i + 1 //下一个函数的索引
let nextFn=dispatch.bind(null, nextFnI) //dispatch传入nextFnI就是要返回 下一个函数的指向
let next = nextFn //生成next参数
fn(next) //执行当前函数,并且传入下一个函数的指向next
}
}
因为为了更容易理解代码,删减了代码很多细节,但是compose函数的思路没有改变。
其中最核心的代码就是生成next并且传入到当前需要执行函数的参数中
dispatch函数的目的就是执行函数
函数执行栈分析
入栈
dispatch(0)就是执行fnList[0]函数,让fnList[0]入函数调用栈,next是fnList[1]函数的指向 ==》执行console.log(1)
- 当
fnList[0]的next(fnLis[1]函数)参数执行时,本质是在函数调用栈中入栈了一个fnLis[1]函数,所以要等fnLis[1]执行完毕才能继续执行fnList[0]函数,next是fnList[2]函数的指向 ==》执行console.log(2)
- 当
fnList[1]的next(fnLis[2]函数)参数执行时,本质是在函数调用栈中入栈了一个fnLis[2]函数,所有要等fnLis[2]执行完毕才能执行fnList[1]函数,next无指向 ==》执行console.log(3)
在函数调用栈中,后进先出,所以最后加入的fnLis[2]函数最先指向完毕
出栈
fnList[2]执行完毕,从函数调用栈中出栈 ==》执行console.log(4)
fnList[1]执行完毕,从函数调用栈中出栈 ==》执行console.log(5)
fnList[2]执行完毕,从函数调用栈中出栈,栈为空,所有函数执行完毕, ==》执行console.log(6)
从①~⑥步依次输出了1-6的数字,模型被实现
总结
对于洋葱模型大多数教程都在讲koa的底层实现,但是对于小白这些都不好去理解,所有本文把洋葱模型单独抽离出来,去除了koa的异步模式,以同步的视角看该模式。无论是同步还是异步,算法的本质都不会变的,只要记住在洋葱模型中参数next指向下一个函数,那么你就懂得这种模式了!!!
如果想更深入了解洋葱模型,可以去看大佬的深入讲解如何更好地理解中间件和洋葱模型