洋蔥模型?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é)果。
這樣說(shuō)的可能還是不太清楚,來(lái)看一下流程圖:
這里是兩步操作,第一步是傳入中間件數(shù)組,第二步是執(zhí)行返回的函數(shù),而我們今天要解析就是第一步。
源碼
源碼并不多,只有不到 50 行,我們來(lái)看一下:
'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) } } } }
雖然說(shuō)不到 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í)是第一次見(jiàn)這樣玩變量提升的,所以先給大家講一下變量提升的規(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
之前。
雖然這樣可行,但是不建議這樣寫(xiě),因?yàn)檫@樣寫(xiě)會(huì)讓代碼變得難以閱讀,不多說(shuō)了,繼續(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!') } // ... }
最開(kāi)始就是洋蔥模型的要求判斷了,中間件必須是數(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
注釋寫(xiě)的很清楚,是最后一個(gè)調(diào)用的中間件的索引,這里初始化為-1
,因?yàn)閿?shù)組的索引是從0
開(kāi)始的。
dispatch
函數(shù)是用來(lái)執(zhí)行中間件的,這里傳入0
,也就是從第一個(gè)中間件開(kāi)始執(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í)行的中間件,通過(guò)索引直接從最開(kāi)始初始化的middleware
數(shù)組里面取出來(lái)。
如果是到了最后一個(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)遞歸的感覺(jué)了,但是并沒(méi)有直接調(diào)用,而是通過(guò)外部手動(dòng)調(diào)用next
來(lái)執(zhí)行下一個(gè)中間件。
這里的try...catch
是為了捕獲中間件執(zhí)行過(guò)程中的錯(cuò)誤,如果有錯(cuò)誤,就返回一個(gè)失敗的Promise
。
動(dòng)手
老規(guī)矩,還是用class
來(lái)實(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)原理就是通過(guò)遞歸來(lái)實(shí)現(xiàn)的,每次執(zhí)行中間件的時(shí)候,都會(huì)返回一個(gè)成功的Promise
。
其實(shí)這里不使用Promise
也是可以的,但是使用Promise
可以有效的處理異步和錯(cuò)誤。
而且從上面手動(dòng)實(shí)現(xiàn)的代碼案例中也可以看到,使用Promise
可以有更多的靈活性,寫(xiě)法也是多元化。
以上就是洋蔥模型 koa-compose源碼解析的詳細(xì)內(nèi)容,更多關(guān)于洋蔥模型 koa-compose的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序?qū)崿F(xiàn)拖拽 image 觸摸事件監(jiān)聽(tīng)的實(shí)例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)拖拽 image 觸摸事件監(jiān)聽(tīng)的實(shí)例的相關(guān)資料,這里提供image觸摸并監(jiān)聽(tīng)的簡(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)資料,這里參考官方文檔寫(xiě)的簡(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