<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
先來聽聽一個故事吧,今天產品提了一個業務需求:使用者在一個編輯頁面,此時使用者點選退出登入,應用需要提示使用者當前有編輯內容未儲存,是否儲存;當用戶操作完畢後再提示使用者是否退出登入。
流程如下:
因為退出登入是屬於公共部分由另一位同學維護,此時和他交流後“善良”的把需求仍給了他。並告知他可以通過某某方法獲取我當前是否有編輯內容。然後我繼續摸魚,他開始瘋狂輸出
const handlerLogout = async () => { if (window.location.href === 'xxx') { if (getEditState() === 'xxx') { await editConfirm() } } await logoutConfirm(); }
功能如約上線,新需求也如約到達:產品期望使用者在VIP充值頁面退出登入的時候,先彈出一個VIP充值廣告,當用戶關閉廣告後再提示使用者是否退出登入。
流程如下:
然後熟悉的場景、熟悉的人,在一番交流過後,那位同學略微暴躁的又開始瘋狂輸出,然後我繼續摸魚
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(); }
然後的然後功能又如約上線,然後需求又來了,一個場景中有多個彈窗業務,優先順序不同,如果彈窗1不滿足彈出條件,就使用彈窗2依此類推。眾所周知產品的需求怎麼做的完,他終於受不了了,開始思考怎麼樣自己才能摸摸魚。與似乎邪惡的想法油然而生,如果自己維護的退出登入就只關注處理退出登入的業務,而其他業務的各種彈窗讓業務方自己去處理那我就可以摸魚啦。想法有了,拆解一下邏輯,底層邏輯就是在觸發時需要有很多中間層的處理,等中間層處理完成後再處理自己的。那這不就像是洋蔥模型嗎。
提到洋蔥模型,koa的實現簡單且優雅。koa中主要使用koa-compose來實現該模式。核心內容只有十幾行,但是卻涉及到高階函數、閉包、遞迴、尾呼叫優化等知識,不得不說非常驚豔沒有一行是多餘的。簡單來說,koa-compose暴露出一個compose方法,該方法接受一箇中介軟體陣列,並返回一個Promise函數。原始碼如下
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主要做了三件事
dispatch函數的作用(dispatch其實就是next函數)
i <= index
來避免在同一個中介軟體中連續next呼叫Promise.resolve()
Promise.resolve
並把當前中介軟體執行結果做為返回,且傳入context和next(dispatch)方法。這裡利用尾調優化,避免了fn重新建立新的棧幀,同時提升了速度和節省了記憶體(大佬就是大佬)我們可以通過其測試用例瞭解到執行的過程,有條件的讀者可以通過下載原始碼進行斷點偵錯,更能理解每一步的過程
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接收一個引數,該引數是一個Promise陣列,注入中介軟體後返回了一個執行函數並執行。此時會按照上訴我標記的步驟進行執行。設定koa檔案中的gif範例和流程圖更好理解。通過不斷的遞迴加上Promise鏈式呼叫完成了整個中介軟體的執行
已經瞭解到洋蔥模型的設計,按照當前摸魚的訴求,期望stack.push這部分內容由業務方自己去注入,而退出登入只需要執行compose(stack)({})即可,額外訴求是專案中期望對彈窗有優先順序的處理,那就是不是誰先進入誰先執行。對此改造一下middleware定義,新增level表示優先順序後續它進行排序,優先順序越高設定level值越高即可。
type Middleware<T = unknown> = { level: number; middleware: (context: T | undefined, next: () => Promise<any>) => void; };
因為我們需要提供給業務方一個介面來新增中介軟體,這裡使用類來實現,通過暴露出add和remove方法對中介軟體進行新增和刪除,利用add方法在新增時利用level對中介軟體進行排序,使用stack來儲存已經排序好的中介軟體。dispatch通過CV大法實現
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;
然後修改業務中的處理,之後再加類似需求就可以摸魚了。
// 暴露一個logoutScheduler方法 export const logoutScheduler = new Scheduler(); const handleLogout = () => { logoutScheduler.dispatch().then(() => { logoutConfirm(); }) } // 編輯頁面 logoutScheduler.add({ level: 2, middleware: async (_, next) => { if (getEditState() === 'xxx') { await editConfirm() } await next(); } }) // vip頁面 logoutScheduler.add({ level: 2, middleware: async (_, next) => { if (getUserVipState() === 'xxx') { await vipConfirm() } await next(); } })
一個好的設計能在實際開發中更好的去解耦業務,而好的設計需要我們去閱讀那些優秀的原始碼去學習和理解才能為我們所用。
以上就是JavaScript 設計模式之洋蔥模型原理及實踐應用的詳細內容,更多關於JavaScript 設計模式洋蔥模型的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45