前端常见的6种设计模式

一.为什么需要理解设计模式?

前端项目会随着需求迭代变得越来越复杂,设计模式的作用就是提前规避 "后期难改、牵一发动全身" 的坑,设计模式的核心价值:解决 "可维护、可扩展" 问题。

1.工厂模式

工厂模式:通过一个统一的 "工厂函数 / 类" 封装对象的创建逻辑,外界只需传入参数(如类型、配置),即可获取所需实例,无需关心实例内部的构造细节。核心是 "创建逻辑与使用逻辑分离",实现批量、灵活地创建相似对象。

前端应用场景

1.Axios 实例

2.Vue实例

3.组件库中的 "表单组件工厂",统一管理所有表单组件的基础属性(如 iddisabled

2.单例模式:确保全局只有一个实例

核心是为了解决 "重复创建实例导致的资源浪费、状态混乱、逻辑冲突" 问题------ 当某个对象在系统中只需要 "唯一存在" 时,单例模式能确保全局访问到的是同一个实例,从根源避免多实例带来的隐患。

前端典型场景:

1.Vuex单一store实例

2.浏览器的 window 对象

3.原型模式:通过 "复制" 创建新对象

原型模式的核心是 "基于已有对象(原型)复制创建新对象" ------ 不是从零开始定义新对象的属性和方法,而是直接 "拷贝" 一个现有对象(原型)的结构,再根据需要修改差异化内容。

前端中原型模式的本质:依托 JavaScript 原型链。

JavaScript 本身就是基于原型的语言,所有对象都有 __proto__ 属性(指向其原型对象),这是原型模式在前端的 "天然实现"。

普通对象原型属性: 只有'proto'属性。

函数原型属性:proto、prototype属性。

prototype专属属性,只有函数有,用于 "当函数作为构造函数时,给新创建的实例提供原型"。

原型链顶端: Object.prototype.proto :指向null ;

前端典型场景:

1.Object.create()

2.Vue2 的数组方法重写:Vue2 为数组的pushpop等方法添加响应式逻辑,新数组会继承这些重写后的方法。

3.继承

工厂模式与原型模式区别:

工厂模式

基于参数 / 规则 "全新创建" 对象;

核心目的:封装复杂的创建逻辑,让调用者无需关心对象构造细节。

原型模式

基于 "已有原型对象" 复制生成新对象

核心目的:复用已有对象的属性 / 方法,减少重复定义,支持继承扩展

4.观察者模式:"一对多" 的依赖通知机制

观察者模式(Observer Pattern)是一种 "一对多" 的依赖关系设计模式:

  • 存在一个 "被观察者(Subject)" 和多个 "观察者(Observer)";
  • 当被观察者的状态发生变化时,会自动通知所有依赖它的观察者,并触发观察者的更新逻辑;
  • 核心是 "解耦被观察者和观察者"------ 双方无需知道彼此的具体实现,只需通过统一的接口通信

前端典型场景:

1.浏览器事件监听(最基础的观察者模式)

浏览器的 DOM 事件本质是观察者模式的实现:

  • 被观察者:DOM 元素(如按钮);
  • 观察者 :事件处理函数(onclickonchange 等);
  • 流程:给元素绑定事件(订阅)→ 元素状态变化(如被点击)→ 自动执行所有绑定的事件处理函数(通知观察者)。
  • 观察者模式的核心价值是 "状态变化自动同步"
2.状态管理库(Vuex/Pinia/Redux)

Vuex、Redux 等全局状态管理库的核心机制就是观察者模式:

  • 被观察者 :Store 中的状态(如 state.userstate.cart);
  • 观察者:依赖该状态的组件;
  • 流程 :组件订阅状态(mapStateuseSelector)→ 状态更新(commitdispatch)→ 所有订阅该状态的组件自动重新渲染(收到通知更新)
3. 框架的响应式系统(Vue/React)

Vue 的响应式原理(数据驱动视图)和 React 的状态更新机制,底层都依赖观察者模式:

  • Vue :数据对象(data)是被观察者,视图(DOM)和计算属性是观察者 ------ 数据变化时,Vue 自动触发依赖收集的观察者(视图重新渲染、计算属性重新计算)。
  • ReactsetState 触发状态更新时,组件树中依赖该状态的组件(观察者)会被重新渲染(收到通知执行更新)。

5.发布-订阅模式

发布 - 订阅模式是观察者模式的变体,核心是通过一个 "中间者(事件中心)" 实现 "发布者" 和 "订阅者" 的完全解耦 ------ 发布者不用知道谁在订阅,订阅者也不用知道谁在发布,双方仅通过事件中心传递消息,就像 "报社(发布者)→ 邮局(事件中心)→ 订报人(订阅者)" 的关系。

  • 三大角色

    1. 发布者(Publisher) :负责 "发布事件"(比如触发某个状态变化,如用户登录、数据更新),但不直接联系订阅者;
    2. 订阅者(Subscriber) :负责 "订阅事件"(比如关注 "用户登录" 事件),并定义事件触发时的 "回调逻辑"(比如登录后显示欢迎信息);
    3. 事件中心(Event Bus) :中间枢纽,负责存储 "事件 - 订阅者" 的映射关系,接收发布者的事件并通知所有订阅者。
  • 核心逻辑:订阅者先在事件中心 "订阅" 某个事件 → 发布者在事件中心 "发布" 该事件 → 事件中心找到所有订阅该事件的订阅者,触发它们的回调。

与观察者模式区别:

维度 观察者模式 发布 - 订阅模式
依赖关系 被观察者直接持有观察者列表 发布者和订阅者无直接依赖,靠事件中心连接
耦合程度 较高(被观察者知道有哪些观察者) 极低(双方不知道彼此存在)
适用场景 单一被观察者、观察者明确的场景 跨模块、多发布者 / 多订阅者的复杂场景
典型例子 Vue 响应式(data 直接通知依赖的 DOM) 跨组件通信(事件总线)、全局状态更新

前端典型场景:

1.跨组件通信(事件总线 Event Bus)

2.全局状态管理(如 Redux 的 Action 机制)

  • 发布者:组件通过 dispatch(action) 发布 "状态变更事件";
  • 事件中心:Redux 的 Store,存储状态并管理订阅者;
  • 订阅者:组件通过 store.subscribe(() => { ... }) 订阅状态变化,状态更新时重新渲染。

状态管理库到底是观察者模式还是发布 - 订阅模式?

状态管理库(如 Vuex、Redux)之所以会让人觉得 "既是观察者模式,又是发布 - 订阅模式",是因为它们融合了两种模式的核心思想 ------ 在底层实现上,既保留了观察者模式 "状态与依赖直接关联" 的特性,又通过 "中间层" 实现了发布 - 订阅模式的 "解耦" 优势,本质是两种模式的结合与优化

1. 底层:状态与组件的 "观察者模式"(直接依赖)

状态管理库中, "全局状态" 与 "依赖该状态的组件" 之间是典型的观察者模式:

  • 被观察者 :全局状态(如 Vuex 的 state、Redux 的 store);
  • 观察者:订阅了该状态的组件;
  • 逻辑:当状态发生变化时,会直接通知所有依赖它的组件(观察者),触发组件重新渲染。

这一层的核心是 "精准依赖 "------ 组件只订阅自己需要的状态(比如 Vue 的 mapState、Redux 的 useSelector),状态变化时只有相关组件会被通知,避免无效更新。

2. 上层:组件与状态的 "发布 - 订阅模式"(解耦通信)

状态管理库中, "组件触发状态变更" 与 "状态变更通知组件" 的过程,通过 "中间层(如 commit/dispatch)" 实现,类似发布 - 订阅模式:

  • 发布者 :触发状态变更的组件(通过 store.commit('increment')dispatch(action) 发布 "状态变更事件");
  • 事件中心 :状态管理库的核心逻辑(如 Vuex 的 Store 实例、Redux 的 dispatch 机制);
  • 订阅者 :依赖状态的组件(通过 subscribe 或计算属性订阅状态)。

这一层的核心是 "解耦"------ 组件不需要知道谁会处理状态变更,也不需要知道哪些组件依赖该状态;状态管理库作为中间层,接收 "发布" 的变更请求,处理后再 "通知" 订阅者,双方完全隔离。

6.代理模式

代理模式(Proxy Pattern)是一种 "通过中间代理对象控制对原始对象的访问" 的设计模式 ------ 不直接操作目标对象,而是通过一个 "代理" 来间接访问,代理可以在访问前后添加额外逻辑(如权限校验、缓存、日志记录等)。

核心作用:"控制访问" 与 "增强功能"

前端典型场景:

1. 权限控制代理(限制访问)
2.Vue3响应式核心
用 "中间商" 的思路理解 Vue3 响应式:
  • 目标对象 :你定义的 data 数据(如 { count: 0, user: { name: '张三' } });
  • 代理对象 :Vue3 通过 reactive()ref() 创建的 "响应式代理"(本质是 Proxy 实例);
  • 调用者 :组件中的模板(视图)或业务逻辑(如 {{ count }}count.value++);
  • 代理的 "附加操作" :拦截数据的读取(get)和修改(set),在读取时 "收集依赖"(记录哪些地方用到了这个数据),在修改时 "触发更新"(通知依赖的地方重新渲染)。
javascript 复制代码
1. 目标对象:原始数据 const target = { count: 0 }; 
2. 依赖收集的容器:记录哪些函数依赖了数据(比如视图渲染函数)
const deps = new Set(); 
3. 创建代理对象(核心:拦截读写,添加响应式逻辑) 
const reactiveProxy = new Proxy(target, 
{
// 拦截"读取数据"操作(如访问 count 时)
get(target, key){
    // 附加操作1:
    收集依赖(假设当前正在执行的函数是依赖) 
    if (currentEffect) { deps.add(currentEffect); // 把依赖存起来 } 
    return target[key]; // 返回原始值 }, 
}
 // 拦截"修改数据"操作(如 count++ 时) 
set(target, key, value) { 
// 更新原始数据 
target[key] = value; 
// 附加操作2:触发更新(通知所有依赖重新执行) 
deps.forEach(effect => effect()); return true; } }); 
}
扩展:Vue3响应式对比vue2响应式

1.Vue2 用的是 Object.defineProperty 拦截属性,只能拦截已存在的属性(对新增属性、数组索引修改不友好);

具体原因拆解:

Object.defineProperty 的工作方式是给对象的某个具体属性添加 getter/setter

但数组本质是特殊对象 (属性是索引,如 arr[0]arr[1]),如果用 Object.defineProperty 拦截数组,只能逐个拦截索引(如 01),但存在两个致命问题:

1.问题一:无法拦截数组的原生方法(push/pop/splice 等) 数组的常用操作(如 push 新增元素、splice 删除元素)是通过调用数组原型上的方法 实现的,这些方法会直接修改数组本身,但 Object.defineProperty 无法拦截 "方法调用",只能拦截 "属性读写"。所以最终Vue2采取了这7个数组方法的重写。

scss 复制代码
 arrayMethods[method] = function(...args) {
    // 先调用原生方法(比如 push 实际添加元素)
    const result = arrayProto[method].apply(this, args);
    // 手动触发更新(通知依赖重新渲染)
    notifyUpdate(); 
    return result;

2.问题二:拦截数组索引的成本极高,且不实用。

  • 初始化成本高:数组长度可能很大(甚至动态变化),提前拦截所有索引会浪费性能;
  • 数组长度变化无法拦截 : 当 arr.length = 0 时,数组会清空所有元素(即删除索引 012),但 Object.defineProperty 只能知道 length 被改成了 0无法知道具体哪些元素被删除了

对于响应式系统来说,需要知道 "哪些元素变化了" 才能精准通知依赖这些元素的视图。但 length 拦截只能知道 "长度变了",无法定位具体变化的元素,导致依赖这些元素的视图可能不会更新(比如某个视图依赖 arr[0]length=0arr[0] 不存在了,但视图可能还显示旧值)。

2.Vue3 用 Proxy 直接代理整个对象,能拦截所有属性的读写(包括新增、删除、数组操作),是更彻底、更灵活的代理模式实现,这也是 Vue3 响应式比 Vue2 强大的核心原因之一。

总结

最后想强调:设计模式不是必须遵守的 "规则",而是解决问题的 "工具"。在实际开发中,我们不需要刻意追求 "用满所有模式",而是根据场景选择合适的工具:

  • 需批量创建对象 → 工厂模式
  • 需全局唯一实例 → 单例模式
  • .....

参考文章:juejin.cn/post/754253...

相关推荐
Lee川15 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix15 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人15 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl15 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人16 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼16 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空16 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_16 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus16 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude