洋蔥模型?koa-compose源碼解析
洋蔥模型
koa-compose是一個非常簡單的函數(shù),它接受一個中間件數(shù)組,返回一個函數(shù),這個函數(shù)就是一個洋蔥模型的核心。
網(wǎng)上一搜一大把圖,我就不貼圖了,代碼也不上,因?yàn)榈葧创a就是,這里只是介紹一下概念。
洋蔥模型是一個非常簡單的概念,它的核心是一個函數(shù),這個函數(shù)接受一個函數(shù)數(shù)組,返回一個函數(shù),這個函數(shù)就是洋蔥模型的核心。
這個返回的函數(shù)就是聚合了所有中間件的函數(shù),它的執(zhí)行順序是從外到內(nèi),從內(nèi)到外。
例如:
- 傳入一個中間件數(shù)組,數(shù)組中有三個中間件,分別是
a、b、c。 - 返回的函數(shù)執(zhí)行時,會先執(zhí)行
a,然后執(zhí)行b,最后執(zhí)行c。 - 執(zhí)行完
c后,會從內(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í)注釋都快占了一半,直接看源碼,先看兩個部分,第一個導(dǎo)出,第二個是返回的函數(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ù)聲明會被提升到函數(shù)頂部。
- 變量提升只會提升變量聲明,不會提升賦值。
- 函數(shù)提升會提升函數(shù)聲明和函數(shù)表達(dá)式。
- 函數(shù)提升會把函數(shù)聲明提升到函數(shù)頂部,函數(shù)表達(dá)式會被提升到變量聲明的位置。
這里的module.exports = compose是變量提升,function compose是函數(shù)提升,所以compose函數(shù)會被提升到module.exports之前。
下面的return dispatch(0)是函數(shù)內(nèi)部的變量提升,dispatch函數(shù)會被提升到return之前。
雖然這樣可行,但是不建議這樣寫,因?yàn)檫@樣寫會讓代碼變得難以閱讀,不多說了,繼續(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)
// ...
}
這個函數(shù)是返回的函數(shù),這個函數(shù)接收兩個參數(shù),context和next,context是上下文,next是下一個中間件,這里的next是compose函數(shù)的第二個參數(shù),也就是app.callback()的第二個參數(shù)。
index注釋寫的很清楚,是最后一個調(diào)用的中間件的索引,這里初始化為-1,因?yàn)閿?shù)組的索引是從0開始的。
dispatch函數(shù)是用來執(zhí)行中間件的,這里傳入0,也就是從第一個中間件開始執(zhí)行。
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
}
可以看到,dispatch函數(shù)接收一個參數(shù),這個參數(shù)是中間件的索引,這里的i就是dispatch(0)傳入的0。
這里的判斷是為了防止next被調(diào)用多次,如果i小于等于index,就會拋出一個錯誤,這里的index是-1,所以這個判斷是不會執(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ù)組里面取出來。
如果是到了最后一個中間件,這里的next指的是下一個中間件,也就是app.callback()的第二個參數(shù)。
如果fn不存在,就返回一個成功的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,也就是下一個中間件。
這里就有點(diǎn)遞歸的感覺了,但是并沒有直接調(diào)用,而是通過外部手動調(diào)用next來執(zhí)行下一個中間件。
這里的try...catch是為了捕獲中間件執(zhí)行過程中的錯誤,如果有錯誤,就返回一個失敗的Promise。
動手
老規(guī)矩,還是用class來實(shí)現(xiàn)一下這個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é)果的截圖了,可以直接瀏覽器控制臺中自行執(zhí)行。
總結(jié)
koa-compose的實(shí)現(xiàn)原理就是通過遞歸來實(shí)現(xiàn)的,每次執(zhí)行中間件的時候,都會返回一個成功的Promise。
其實(shí)這里不使用Promise也是可以的,但是使用Promise可以有效的處理異步和錯誤。
而且從上面手動實(shí)現(xiàn)的代碼案例中也可以看到,使用Promise可以有更多的靈活性,寫法也是多元化。
以上就是洋蔥模型 koa-compose源碼解析的詳細(xì)內(nèi)容,更多關(guān)于洋蔥模型 koa-compose的資料請關(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)聽的簡單實(shí)例,需要的朋友可以參考下2017-08-08
微信小程序 實(shí)現(xiàn)動態(tài)顯示和隱藏某個控件
這篇文章主要介紹了微信小程序 實(shí)現(xiàn)動態(tài)顯示和隱藏某個控件的相關(guān)資料,需要的朋友可以參考下2017-04-04
微信小程序 ecshop地址三級聯(lián)動實(shí)現(xiàn)實(shí)例代碼
這篇文章主要介紹了微信小程序 ecshop地址3級聯(lián)動實(shí)現(xiàn)實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02

