前端常见的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...

相关推荐
zyfts5 小时前
手把手教学用nodejs读写飞书在线表格
前端
www_stdio5 小时前
深入浅出 JavaScript:从对象字面量到代理模式的实践探索
javascript
泉城老铁5 小时前
vue实现前端excel的导出
前端·vue.js
用户51681661458415 小时前
Lottie动画在前端web、vue、react中使用详解
前端·vue.js
EndingCoder6 小时前
Node.js SQL数据库:MySQL/PostgreSQL集成
javascript·数据库·sql·mysql·postgresql·node.js
咖啡の猫6 小时前
Vue收集表单数据
前端·javascript·vue.js
知识分享小能手6 小时前
uni-app 入门学习教程,从入门到精通, uni-app常用API的详细语法知识点(上)(5)
前端·javascript·vue.js·学习·微信小程序·小程序·uni-app
林恒smileZAZ6 小时前
CSS3 超实用属性:pointer-events (可穿透图层的鼠标事件)
前端·计算机外设·css3
云中雾丽6 小时前
flutter中 NotificationListener 详细使用指南
前端