JavaScript?設(shè)計(jì)模式之洋蔥模型原理及實(shí)踐應(yīng)用
前言
先來(lái)聽(tīng)聽(tīng)一個(gè)故事吧,今天產(chǎn)品提了一個(gè)業(yè)務(wù)需求:用戶(hù)在一個(gè)編輯頁(yè)面,此時(shí)用戶(hù)點(diǎn)擊退出登錄,應(yīng)用需要提示用戶(hù)當(dāng)前有編輯內(nèi)容未保存,是否保存;當(dāng)用戶(hù)操作完畢后再提示用戶(hù)是否退出登錄。
流程如下:
因?yàn)橥顺龅卿浭菍儆诠膊糠钟闪硪晃煌瑢W(xué)維護(hù),此時(shí)和他交流后“善良”的把需求仍給了他。并告知他可以通過(guò)某某方法獲取我當(dāng)前是否有編輯內(nèi)容。然后我繼續(xù)摸魚(yú),他開(kāi)始瘋狂輸出
const handlerLogout = async () => { if (window.location.href === 'xxx') { if (getEditState() === 'xxx') { await editConfirm() } } await logoutConfirm(); }
功能如約上線(xiàn),新需求也如約到達(dá):產(chǎn)品期望用戶(hù)在VIP充值頁(yè)面退出登錄的時(shí)候,先彈出一個(gè)VIP充值廣告,當(dāng)用戶(hù)關(guān)閉廣告后再提示用戶(hù)是否退出登錄。
流程如下:
然后熟悉的場(chǎng)景、熟悉的人,在一番交流過(guò)后,那位同學(xué)略微暴躁的又開(kāi)始瘋狂輸出,然后我繼續(xù)摸魚(yú)
const pages = { editPage: async () => { if (getEditState() === 'xxx') { await editConfirm() } }, vipPage: async () => { if (getUserVipState() === 'xxx') { await vipConfirm() } } } const handlerLogout = async () => { const curPage = getPage(); await pages[curPage]; await logoutConfirm(); }
然后的然后功能又如約上線(xiàn),然后需求又來(lái)了,一個(gè)場(chǎng)景中有多個(gè)彈窗業(yè)務(wù),優(yōu)先級(jí)不同,如果彈窗1不滿(mǎn)足彈出條件,就使用彈窗2依此類(lèi)推。眾所周知產(chǎn)品的需求怎么做的完,他終于受不了了,開(kāi)始思考怎么樣自己才能摸摸魚(yú)。與似乎不好的想法油然而生,如果自己維護(hù)的退出登錄就只關(guān)注處理退出登錄的業(yè)務(wù),而其他業(yè)務(wù)的各種彈窗讓業(yè)務(wù)方自己去處理那我就可以摸魚(yú)啦。想法有了,拆解一下邏輯,底層邏輯就是在觸發(fā)時(shí)需要有很多中間層的處理,等中間層處理完成后再處理自己的。那這不就像是洋蔥模型嗎。
洋蔥模型
提到洋蔥模型,koa的實(shí)現(xiàn)簡(jiǎn)單且優(yōu)雅。koa中主要使用koa-compose來(lái)實(shí)現(xiàn)該模式。核心內(nèi)容只有十幾行,但是卻涉及到高階函數(shù)、閉包、遞歸、尾調(diào)用優(yōu)化等知識(shí),不得不說(shuō)非常驚艷沒(méi)有一行是多余的。簡(jiǎn)單來(lái)說(shuō),koa-compose暴露出一個(gè)compose方法,該方法接受一個(gè)中間件數(shù)組,并返回一個(gè)Promise函數(shù)。源碼如下
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) } } } }
源碼中compose主要做了三件事
- 第一步:進(jìn)行入?yún)⑿r?yàn)
- 第二步:返回一個(gè)函數(shù),并利用閉包保存middleware和index的值
- 第三步:調(diào)用時(shí),執(zhí)行dispatch(0),默認(rèn)從第一個(gè)中間件執(zhí)行
dispatch函數(shù)的作用(dispatch其實(shí)就是next函數(shù))
- 第一步:通過(guò)
i <= index
來(lái)避免在同一個(gè)中間件中連續(xù)next調(diào)用 - 第二步:設(shè)置index的值為當(dāng)前中間件位置的值,并且拿到當(dāng)前中間件函數(shù)
- 第三步:判斷當(dāng)前是否還有中間件,沒(méi)有返回
Promise.resolve()
- 第四步:返回
Promise.resolve
并把當(dāng)前中間件執(zhí)行結(jié)果做為返回,且傳入context和next(dispatch)方法。這里利用尾調(diào)優(yōu)化,避免了fn重新創(chuàng)建新的棧幀,同時(shí)提升了速度和節(jié)省了內(nèi)存(大佬就是大佬)
我們可以通過(guò)其測(cè)試用例了解到執(zhí)行的過(guò)程,有條件的讀者可以通過(guò)下載源碼進(jìn)行斷點(diǎn)調(diào)試,更能理解每一步的過(guò)程
it('should work', async () => { const arr = [] const stack = [] stack.push(async (context, next) => { arr.push(1) // 步驟1 await wait(1) // 步驟2 await next() // 步驟3 await wait(1) // 步驟14 arr.push(6) // 步驟15 }) stack.push(async (context, next) => { arr.push(2) // 步驟4 await wait(1) // 步驟5 await next() // 步驟6 await wait(1) // 步驟12 arr.push(5) // 步驟13 }) stack.push(async (context, next) => { arr.push(3) // 步驟7 await wait(1) // 步驟8 await next() // 步驟9 await wait(1) // 步驟10 arr.push(4) // 步驟11 }) await compose(stack)({}) expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6])) })
compose接收一個(gè)參數(shù),該參數(shù)是一個(gè)Promise數(shù)組,注入中間件后返回了一個(gè)執(zhí)行函數(shù)并執(zhí)行。此時(shí)會(huì)按照上訴我標(biāo)記的步驟進(jìn)行執(zhí)行。配置koa文檔中的gif示例和流程圖更好理解。通過(guò)不斷的遞歸加上Promise鏈?zhǔn)秸{(diào)用完成了整個(gè)中間件的執(zhí)行
實(shí)踐
已經(jīng)了解到洋蔥模型的設(shè)計(jì),按照當(dāng)前摸魚(yú)的訴求,期望stack.push這部分內(nèi)容由業(yè)務(wù)方自己去注入,而退出登錄只需要執(zhí)行compose(stack)({})即可,額外訴求是項(xiàng)目中期望對(duì)彈窗有優(yōu)先級(jí)的處理,那就是不是誰(shuí)先進(jìn)入誰(shuí)先執(zhí)行。對(duì)此改造一下middleware定義,新增level表示優(yōu)先級(jí)后續(xù)它進(jìn)行排序,優(yōu)先級(jí)越高設(shè)置level值越高即可。
type Middleware<T = unknown> = { level: number; middleware: (context: T | undefined, next: () => Promise<any>) => void; };
因?yàn)槲覀冃枰峁┙o業(yè)務(wù)方一個(gè)接口來(lái)添加中間件,這里使用類(lèi)來(lái)實(shí)現(xiàn),通過(guò)暴露出add和remove方法對(duì)中間件進(jìn)行添加和刪除,利用add方法在添加時(shí)利用level對(duì)中間件進(jìn)行排序,使用stack來(lái)保存已經(jīng)排序好的中間件。dispatch通過(guò)CV大法實(shí)現(xiàn)
class Scheduler<T> { stack: Middleware<T>[] = []; add(middleware: Middleware<T>) { const index = this.stack.findIndex((it) => it.level <= middleware.level); this.stack.splice(index === -1 ? this.stack.length : index, 0, middleware); return () => { this.remove(middleware); }; } remove(middleware: Middleware<T>) { const index = this.stack.findIndex((it) => it === middleware); index > -1 && this.stack.splice(index, 1); } dispatch(context?: T) { // eslint-disable-next-line const that = this; let index = -1; return mutate(0); function mutate(i: number): Promise<void> { if (i <= index) return Promise.reject(new Error('next() called multiple times')); index = i; const fn = that.stack[i]; if (index === that.stack.length) return Promise.resolve(); try { return Promise.resolve(fn.middleware(context, mutate.bind(null, i + 1))); } catch (error) { return Promise.reject(error); } } } } export default Scheduler;
然后修改業(yè)務(wù)中的處理,之后再加類(lèi)似需求就可以摸魚(yú)了。
// 暴露一個(gè)logoutScheduler方法 export const logoutScheduler = new Scheduler(); const handleLogout = () => { logoutScheduler.dispatch().then(() => { logoutConfirm(); }) } // 編輯頁(yè)面 logoutScheduler.add({ level: 2, middleware: async (_, next) => { if (getEditState() === 'xxx') { await editConfirm() } await next(); } }) // vip頁(yè)面 logoutScheduler.add({ level: 2, middleware: async (_, next) => { if (getUserVipState() === 'xxx') { await vipConfirm() } await next(); } })
總結(jié)
一個(gè)好的設(shè)計(jì)能在實(shí)際開(kāi)發(fā)中更好的去解耦業(yè)務(wù),而好的設(shè)計(jì)需要我們?nèi)ラ喿x那些優(yōu)秀的源碼去學(xué)習(xí)和理解才能為我們所用。
以上就是JavaScript 設(shè)計(jì)模式之洋蔥模型原理及實(shí)踐應(yīng)用的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 設(shè)計(jì)模式洋蔥模型的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 頁(yè)面跳轉(zhuǎn)如何實(shí)現(xiàn)傳值
這篇文章主要介紹了微信小程序 頁(yè)面跳轉(zhuǎn)如何實(shí)現(xiàn)傳值的相關(guān)資料,需要的朋友可以參考下2017-04-04Lodash加減乘除add?subtract?multiply?divide方法源碼解讀
這篇文章主要介紹了Lodash加減乘除add?subtract?multiply?divide方法源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05Web?Components實(shí)現(xiàn)類(lèi)Element?UI中的Card卡片
這篇文章主要為大家介紹了Web?Components實(shí)現(xiàn)類(lèi)Element?UI中的Card卡片實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07實(shí)現(xiàn)基于飛書(shū)webhook監(jiān)聽(tīng)github代碼提交
這篇文章主要為大家介紹了實(shí)現(xiàn)基于飛書(shū)webhook監(jiān)聽(tīng)github代碼提交示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01前端可視化搭建定義聯(lián)動(dòng)協(xié)議實(shí)現(xiàn)
這篇文章主要為大家介紹了前端可視化搭建定義聯(lián)動(dòng)協(xié)議實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05Javascript設(shè)計(jì)模式之原型模式詳細(xì)
這篇文章主要介紹了Javascript設(shè)計(jì)模式之原型模式,原型模式用于在創(chuàng)建對(duì)象時(shí),通過(guò)共享某個(gè)對(duì)象原型的屬性和方法,從而達(dá)到提高性能、降低內(nèi)存占用、代碼復(fù)用的效果。下面小編將詳細(xì)介紹 ,需要的朋友可以參考下2021-09-09