设计模式实战:单例・发布订阅・策略 JS 轻量用法|JS 进阶必会篇
设计模式(单例/发布订阅/策略)】+【前端中后台开发】:从【核心概念】到【落地实操】,彻底搞懂前端常用设计模式的最佳写法,避开测试残留、内存泄漏、面条码高频坑!
📑 文章目录
- 一、开篇:为什么要学设计模式?
- 二、概念扫盲
- 三、单例模式:权限控制里的“唯一管家”
- 3.1 是什么?
- 3.2 最简单的实现
- 3.3 用 class 实现(更贴近日常写法)
- 3.4 实际用法:按钮权限、路由守卫
- 3.5 单例的坑
- 四、发布订阅模式:消息通知解耦
- 4.1 是什么?
- 4.2 核心:EventBus
- 4.3 实战:消息通知中心
- 4.4 完整示例:登录后多处联动
- 4.5 发布订阅的坑
- 五、策略模式:表单校验规则可插拔
- 5.1 是什么?
- 5.2 没策略时:容易变成“面条码”
- 5.3 用策略重构
- 5.4 实战:表单校验配置化
- 5.5 策略的坑
- 六、三者怎么选?
- 七、小结
- 🔍 本系列专栏导航
同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
一、开篇:为什么要学设计模式?
你可能会想:我写业务能跑就行,为什么要管设计模式?
实际工作中常见这些情况:
权限控制:路由守卫、按钮权限、接口权限各处都在写,逻辑重复、难维护。
消息通知:多个模块要弹 Toast,互相耦合,改一处影响一片。
表单校验:不同字段用不同规则,全写在一个大
if-else里,难扩展。
这些问题都可以用少量设计模式来简化。下面用单例、发布订阅、策略三种模式,搭配权限、通知、表单校验三个场景,讲清楚:日常怎么写、为什么这么写、容易踩哪些坑。
⬆ 返回目录
二、概念扫盲
先把三个模式用一句话说清楚:
| 模式 | 一句话 | 适合场景 |
|---|---|---|
| 单例 | 全局只存在一个实例,多次调用拿到同一个对象 | 全局唯一的东西:权限管理器、通知中心、全局配置 |
| 发布订阅 | 发布者发事件,订阅者监听,彼此解耦 | 一对多、多对多通知:消息通知、事件总线 |
| 策略 | 把多种“算法/规则”封装成可替换的策略 | 同种操作多种规则:表单校验、支付方式选择 |
下面按「概念 → 代码 → 实战」来展开。
⬆ 返回目录
三、单例模式:权限控制里的“唯一管家”
3.1 是什么?
单例保证:不管你怎么new、怎么调用,拿到的一定是同一个实例。
⬆ 返回目录
3.2 最简单的实现
// 懒汉式单例:用到的时候才创建functioncreatePermissionManager(){letinstance=null;returnfunction(){if(!instance){instance={role:'guest',permissions:[],check(perm){returnthis.permissions.includes(perm);},setRole(role,perms){this.role=role;this.permissions=perms;}};}returninstance;};}constgetPermissionManager=createPermissionManager();constpm1=getPermissionManager();constpm2=getPermissionManager();console.log(pm1===pm2);// true核心是闭包,如果有同学不清楚闭包是什么?闭包怎么用?点击👉这里👈学习一下吧。
⬆ 返回目录
3.3 用 class 实现(更贴近日常写法)
classPermissionManager{staticinstance=null;constructor(){if(PermissionManager.instance){returnPermissionManager.instance;}this.role='guest';this.permissions=[];PermissionManager.instance=this;}check(perm){returnthis.permissions.includes(perm);}setRole(role,perms){this.role=role;this.permissions=perms;}}// 无论怎么 new,都是同一个constpm1=newPermissionManager();constpm2=newPermissionManager();console.log(pm1===pm2);// true⬆ 返回目录
3.4 实际用法:按钮权限、路由守卫
// permission.js - 全局唯一的权限管理器classPermissionManager{staticinstance=null;constructor(){if(PermissionManager.instance)returnPermissionManager.instance;this.permissions=[];PermissionManager.instance=this;}init(perms){this.permissions=perms;}has(perm){returnthis.permissions.includes(perm);}// 用于 v-if 指令:<button v-if="permission.has('user:delete')">hasPermission(perm){return()=>this.has(perm);}}exportconstpermission=newPermissionManager();// 在路由守卫里用// router.beforeEach((to, from, next) => {// if (to.meta.perm && !permission.has(to.meta.perm)) {// next('/403');// return;// }// next();// });要点:路由、按钮、接口都用同一个permission实例,权限数据统一维护,避免到处复制逻辑。
⬆ 返回目录
3.5 单例的坑
| 坑 | 原因 | 建议 |
|---|---|---|
| 测试时状态残留 | 单例在测试间共享 | 提供reset()或在测试前permission.permissions = [] |
| 滥用单例 | 不是全局唯一的东西也做成单例 | 只对真正“全局唯一”的用单例 |
| 忘了初始化 | 直接用check但没init | 在登录成功后统一permission.init(perms) |
⬆ 返回目录
四、发布订阅模式:消息通知解耦
4.1 是什么?
发布者发事件,订阅者订阅事件,彼此不直接依赖。发布者不关心谁在监听,订阅者不关心谁在发。
⬆ 返回目录
4.2 核心:EventBus
classEventBus{constructor(){this.events={};// { eventName: [fn1, fn2, ...] }}on(eventName,fn){if(!this.events[eventName]){this.events[eventName]=[];}this.events[eventName].push(fn);}off(eventName,fn){if(!this.events[eventName])return;this.events[eventName]=this.events[eventName].filter(cb=>cb!==fn);}emit(eventName,...args){if(!this.events[eventName])return;this.events[eventName].forEach(fn=>fn(...args));}}⬆ 返回目录
4.3 实战:消息通知中心
// 通知中心:单例 + 发布订阅classNotificationCenter{staticinstance=null;constructor(){if(NotificationCenter.instance)returnNotificationCenter.instance;this.events={};NotificationCenter.instance=this;}// 订阅某种类型的通知on(type,handler){if(!this.events[type])this.events[type]=[];this.events[type].push(handler);}off(type,handler){if(!this.events[type])return;this.events[type]=this.events[type].filter(h=>h!==handler);}// 发布:触发所有订阅者emit(type,payload){if(!this.events[type])return;this.events[type].forEach(handler=>handler(payload));}}constnotify=newNotificationCenter();// 业务 A:订单成功notify.on('orderSuccess',(orderId)=>{Toast.success(`订单${orderId}创建成功`);// 可能还要更新购物车、统计等});// 业务 B:支付成功notify.on('paymentSuccess',(data)=>{Toast.success('支付成功');router.push('/orders');});// 某处触发notify.emit('orderSuccess','ORD123');⬆ 返回目录
4.4 完整示例:登录后多处联动
// 用户登录成功后,多个模块要同时反应notify.on('loginSuccess',(user)=>{// 模块 1:更新 header 头像header.updateAvatar(user.avatar);});notify.on('loginSuccess',(user)=>{// 模块 2:拉取用户权限permission.init(user.permissions);});notify.on('loginSuccess',()=>{// 模块 3:刷新待办数量todoBadge.refresh();});// 登录接口成功后,只发一次loginApi().then(user=>{notify.emit('loginSuccess',user);});发布者只管emit,订阅者各自处理,互不依赖,修改一处不影响其他模块。
⬆ 返回目录
4.5 发布订阅的坑
| 坑 | 原因 | 建议 |
|---|---|---|
| 内存泄漏 | 组件销毁后没off | 在beforeUnmount里统一off |
| 事件名魔法字符串 | 到处写'orderSuccess'易 typo | 抽成常量EVENTS.ORDER_SUCCESS |
| 过度解耦 | 简单父子通信也用 EventBus | 能用 props/emit 就用,只在跨层、多对多时用 |
| 回调地狱 | 用事件代替 Promise | 异步流程优先用 async/await,事件只做“通知” |
⬆ 返回目录
五、策略模式:表单校验规则可插拔
5.1 是什么?
把不同校验规则封装成独立策略,用配置或映射表选择执行哪个,避免大段if-else。
⬆ 返回目录
5.2 没策略时:容易变成“面条码”
// 反面教材:每加一个规则就要改这里functionvalidate(value,rule){if(rule==='required'){returnvalue!==''&&value!=null;}if(rule==='email'){return/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(value);}if(rule==='phone'){return/^1\d{10}$/.test(value);}if(rule==='minLength'){returnvalue.length>=6;}// 越加越多...}⬆ 返回目录
5.3 用策略重构
// 策略对象:每个规则是独立函数conststrategies={required(value){returnvalue!==''&&value!=null&&String(value).trim()!=='';},email(value){return/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(value);},phone(value){return/^1\d{10}$/.test(value);},minLength(value,min){return(value||'').length>=min;},maxLength(value,max){return(value||'').length<=max;}};// 校验器:根据规则名调用对应策略functionvalidate(value,ruleName,...ruleArgs){constfn=strategies[ruleName];if(!fn)returntrue;// 未知规则默认通过returnfn(value,...ruleArgs);}// 使用validate('','required');// falsevalidate('a@b.com','email');// truevalidate('12345','minLength',6);// false⬆ 返回目录
5.4 实战:表单校验配置化
// 表单校验配置constformRules={username:[{strategy:'required',message:'用户名不能为空'},{strategy:'minLength',params:[3],message:'至少 3 个字符'}],email:[{strategy:'required',message:'邮箱不能为空'},{strategy:'email',message:'邮箱格式不正确'}],phone:[{strategy:'phone',message:'手机号格式不正确'}]};functionvalidateForm(formData){consterrors={};for(const[field,rules]ofObject.entries(formRules)){for(construleofrules){const{strategy,params=[],message}=rule;constvalue=formData[field];constvalid=validate(value,strategy,...params);if(!valid){errors[field]=message;break;// 一个字段只保留第一个错误}}}return{valid:Object.keys(errors).length===0,errors};}// 使用constresult=validateForm({username:'ab',email:'invalid',phone:'13800138000'});console.log(result);// { valid: false, errors: { username: '至少 3 个字符', email: '邮箱格式不正确' } }新增字段或规则时,只改配置,不改validate核心逻辑。
⬆ 返回目录
5.5 策略的坑
| 坑 | 原因 | 建议 |
|---|---|---|
| 策略与业务混在一起 | 策略里写请求、跳转 | 策略只做“规则判断”,返回 boolean |
| 规则参数传错 | minLength要数字,传了字符串 | 做参数校验或封装成createMinLength(6) |
| 和单例搞混 | 策略不需要全局唯一 | 策略是无状态的纯函数,不共享实例 |
⬆ 返回目录
六、三者怎么选?
| 场景 | 推荐模式 | 理由 |
|---|---|---|
| 全局唯一:权限、配置、通知中心 | 单例 | 避免多处实例、状态不一致 |
| 一对多/多对多:登录联动、订单状态 | 发布订阅 | 解耦,扩展新监听者不改原有逻辑 |
| 多种规则:表单校验、支付方式、折扣计算 | 策略 | 规则可插拔,易维护和扩展 |
可以组合用,例如:单例的通知中心 + 发布订阅,或策略模式 + 单例的校验器。
⬆ 返回目录
七、小结
| 模式 | 一句话 | 典型用法 |
|---|---|---|
| 单例 | 全局只一个实例 | 权限管理器、通知中心、全局配置 |
| 发布订阅 | 发布事件、订阅处理,彼此解耦 | 消息通知、登录后联动、跨模块通信 |
| 策略 | 多种规则封装成可替换策略 | 表单校验、支付方式、折扣规则 |
记住三点:
单例只给“真正全局唯一”的东西用,并注意测试时重置。
发布订阅适合跨模块通知,简单通信优先用 props/emit,用完记得
off。策略模式把规则抽成独立函数,用配置驱动,方便扩展。
设计模式不是炫技,而是让代码更好改、更好测、更少 bug 的工具。先能用、再好用,逐步引入即可。
⬆ 返回目录
🔍 本系列专栏导航
📝 JS 进阶必会点
一、《闭包实战:从原理到防抖・节流・缓存|JS 进阶必会篇》
二、《异步基础:Promise & async/await 实战|JS 进阶必会篇》
三、《常用工具函数:深拷贝・去重・扁平化手写实战|JS 进阶必会篇》
四、《事件循环与宏微任务:log 顺序实战解析|JS 进阶必会篇》
五、《设计模式实战:单例・发布订阅・策略 JS 轻量用法|JS 进阶必会篇》
六、《浏览器存储实战:localStorage/sessionStorage/cookie 用法详解|JS 进阶必会篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
💡 更多内容(TS/Vue/工程化等共47篇)已整理成「全体系总目录」👇,收藏后可一站式学习:
📚 系列总览
想系统学习Vue3中后台开发?收藏这份「全体系指南」,47篇干货从基础到工程化全覆盖:
《前端基础实战:JS/TS与Vue体系化扫盲(47 篇完整目录 + 避坑)》
💡 每篇都配套实战场景+避坑指南,帮你摆脱「面向搜索引擎写代码」的尴尬~
⬆ 返回目录
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~
