洋蔥模型?koa-compose源碼解析
洋蔥模型
koa-compose是一個(gè)非常簡(jiǎn)單的函數(shù),它接受一個(gè)中間件數(shù)組,返回一個(gè)函數(shù),這個(gè)函數(shù)就是一個(gè)洋蔥模型的核心。
網(wǎng)上一搜一大把圖,我就不貼圖了,代碼也不上,因?yàn)榈葧?huì)源碼就是,這里只是介紹一下概念。
洋蔥模型是一個(gè)非常簡(jiǎn)單的概念,它的核心是一個(gè)函數(shù),這個(gè)函數(shù)接受一個(gè)函數(shù)數(shù)組,返回一個(gè)函數(shù),這個(gè)函數(shù)就是洋蔥模型的核心。
這個(gè)返回的函數(shù)就是聚合了所有中間件的函數(shù),它的執(zhí)行順序是從外到內(nèi),從內(nèi)到外。
例如:
- 傳入一個(gè)中間件數(shù)組,數(shù)組中有三個(gè)中間件,分別是
a、b、c。 - 返回的函數(shù)執(zhí)行時(shí),會(huì)先執(zhí)行
a,然后執(zhí)行b,最后執(zhí)行c。 - 執(zhí)行完
c后,會(huì)從內(nèi)到外依次執(zhí)行b、a。 - 執(zhí)行完
a后,返回執(zhí)行結(jié)果。
這樣說的可能還是不太清楚,來看一下流程圖:

這里是兩步操作,第一步是傳入中間件數(shù)組,第二步是執(zhí)行返回的函數(shù),而我們今天要解析就是第一步。
源碼
源碼并不多,只有不到 50 行,我們來看一下:
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* 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)
}
}
}
}
雖然說不到 50 行,其實(shí)注釋都快占了一半,直接看源碼,先看兩個(gè)部分,第一個(gè)導(dǎo)出,第二個(gè)是返回的函數(shù)。
module.exports = compose
function compose (middleware) {
// ...
return function (context, next) {
return dispatch(0)
function dispatch (i) {
// ...
}
}
}
這里確實(shí)是第一次見這樣玩變量提升的,所以先給大家講一下變量提升的規(guī)則:
- 變量提升是在函數(shù)執(zhí)行前,函數(shù)內(nèi)部的變量和函數(shù)聲明會(huì)被提升到函數(shù)頂部。
- 變量提升只會(huì)提升變量聲明,不會(huì)提升賦值。
- 函數(shù)提升會(huì)提升函數(shù)聲明和函數(shù)表達(dá)式。
- 函數(shù)提升會(huì)把函數(shù)聲明提升到函數(shù)頂部,函數(shù)表達(dá)式會(huì)被提升到變量聲明的位置。
這里的module.exports = compose是變量提升,function compose是函數(shù)提升,所以compose函數(shù)會(huì)被提升到module.exports之前。
下面的return dispatch(0)是函數(shù)內(nèi)部的變量提升,dispatch函數(shù)會(huì)被提升到return之前。
雖然這樣可行,但是不建議這樣寫,因?yàn)檫@樣寫會(huì)讓代碼變得難以閱讀,不多說了,繼續(xù)吧:
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!')
}
// ...
}
最開始就是洋蔥模型的要求判斷了,中間件必須是數(shù)組,數(shù)組里面的每一項(xiàng)必須是函數(shù)。
繼續(xù)看:
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
// ...
}
這個(gè)函數(shù)是返回的函數(shù),這個(gè)函數(shù)接收兩個(gè)參數(shù),context和next,context是上下文,next是下一個(gè)中間件,這里的next是compose函數(shù)的第二個(gè)參數(shù),也就是app.callback()的第二個(gè)參數(shù)。
index注釋寫的很清楚,是最后一個(gè)調(diào)用的中間件的索引,這里初始化為-1,因?yàn)閿?shù)組的索引是從0開始的。
dispatch函數(shù)是用來執(zhí)行中間件的,這里傳入0,也就是從第一個(gè)中間件開始執(zhí)行。
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
}
可以看到,dispatch函數(shù)接收一個(gè)參數(shù),這個(gè)參數(shù)是中間件的索引,這里的i就是dispatch(0)傳入的0。
這里的判斷是為了防止next被調(diào)用多次,如果i小于等于index,就會(huì)拋出一個(gè)錯(cuò)誤,這里的index是-1,所以這個(gè)判斷是不會(huì)執(zhí)行的。
后面就賦值了index為i,這樣就可以防止next被調(diào)用多次了,繼續(xù)看:
let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve()
這里的fn是中間件,也是當(dāng)前要執(zhí)行的中間件,通過索引直接從最開始初始化的middleware數(shù)組里面取出來。
如果是到了最后一個(gè)中間件,這里的next指的是下一個(gè)中間件,也就是app.callback()的第二個(gè)參數(shù)。
如果fn不存在,就返回一個(gè)成功的Promise,表示所有的中間件都執(zhí)行完了。
繼續(xù)看:
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
這里就是執(zhí)行中間件的地方了,fn是剛才取到的中間件,直接執(zhí)行。
然后傳入context和dispatch.bind(null, i + 1),這里的dispatch.bind(null, i + 1)就是next,也就是下一個(gè)中間件。
這里就有點(diǎn)遞歸的感覺了,但是并沒有直接調(diào)用,而是通過外部手動(dòng)調(diào)用next來執(zhí)行下一個(gè)中間件。
這里的try...catch是為了捕獲中間件執(zhí)行過程中的錯(cuò)誤,如果有錯(cuò)誤,就返回一個(gè)失敗的Promise。
動(dòng)手
老規(guī)矩,還是用class來實(shí)現(xiàn)一下這個(gè)compose函數(shù)。
class Compose {
constructor(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!')
}
this.index = -1
this.middleware = middleware
return (next) => {
this.next = next
return this.dispatch(0)
}
}
dispatch(i) {
if (i <= this.index) return Promise.reject(new Error('next() called multiple times'))
this.index = i
let fn = this.middleware[i]
if (i === this.middleware.length) fn = this.next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(this.dispatch.bind(this, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
var middleware = [
(next) => {
console.log(1)
next()
console.log(2)
},
(next) => {
console.log(3)
next()
console.log(4)
},
(next) => {
console.log(5)
next()
console.log(6)
}
]
var compose = new Compose(middleware)
compose()
var middleware = [
(next) => {
return next().then((res) => {
return res + '1'
})
},
(next) => {
return next().then((res) => {
return res + '2'
})
},
(next) => {
return next().then((res) => {
return res + '3'
})
}
]
var compose = new Compose(middleware)
compose(() => {
return Promise.resolve('0')
}).then((res) => {
console.log(res)
})
這次不放執(zhí)行結(jié)果的截圖了,可以直接瀏覽器控制臺(tái)中自行執(zhí)行。
總結(jié)
koa-compose的實(shí)現(xiàn)原理就是通過遞歸來實(shí)現(xiàn)的,每次執(zhí)行中間件的時(shí)候,都會(huì)返回一個(gè)成功的Promise。
其實(shí)這里不使用Promise也是可以的,但是使用Promise可以有效的處理異步和錯(cuò)誤。
而且從上面手動(dòng)實(shí)現(xiàn)的代碼案例中也可以看到,使用Promise可以有更多的靈活性,寫法也是多元化。
以上就是洋蔥模型 koa-compose源碼解析的詳細(xì)內(nèi)容,更多關(guān)于洋蔥模型 koa-compose的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序?qū)崿F(xiàn)拖拽 image 觸摸事件監(jiān)聽的實(shí)例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)拖拽 image 觸摸事件監(jiān)聽的實(shí)例的相關(guān)資料,這里提供image觸摸并監(jiān)聽的簡(jiǎn)單實(shí)例,需要的朋友可以參考下2017-08-08
微信小程序 實(shí)現(xiàn)動(dòng)態(tài)顯示和隱藏某個(gè)控件
這篇文章主要介紹了微信小程序 實(shí)現(xiàn)動(dòng)態(tài)顯示和隱藏某個(gè)控件的相關(guān)資料,需要的朋友可以參考下2017-04-04
微信小程序 支付簡(jiǎn)單實(shí)例及注意事項(xiàng)
這篇文章主要介紹了微信小程序 支付簡(jiǎn)單實(shí)例的相關(guān)資料,這里參考官方文檔寫的簡(jiǎn)單實(shí)例,并提出注意事項(xiàng),需要的朋友可以參考下2017-01-01
微信小程序 ecshop地址三級(jí)聯(lián)動(dòng)實(shí)現(xiàn)實(shí)例代碼
這篇文章主要介紹了微信小程序 ecshop地址3級(jí)聯(lián)動(dòng)實(shí)現(xiàn)實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02

