一.为什么需要理解设计模式?
前端项目会随着需求迭代变得越来越复杂,设计模式的作用就是提前规避 "后期难改、牵一发动全身" 的坑,设计模式的核心价值:解决 "可维护、可扩展" 问题。
1.工厂模式
工厂模式:通过一个统一的 "工厂函数 / 类" 封装对象的创建逻辑,外界只需传入参数(如类型、配置),即可获取所需实例,无需关心实例内部的构造细节。核心是 "创建逻辑与使用逻辑分离",实现批量、灵活地创建相似对象。
前端应用场景:
1.Axios 实例
2.Vue实例
3.组件库中的 "表单组件工厂",统一管理所有表单组件的基础属性(如 id
、disabled
)
2.单例模式:确保全局只有一个实例
核心是为了解决 "重复创建实例导致的资源浪费、状态混乱、逻辑冲突" 问题------ 当某个对象在系统中只需要 "唯一存在" 时,单例模式能确保全局访问到的是同一个实例,从根源避免多实例带来的隐患。
前端典型场景:
1.Vuex单一store实例
2.浏览器的 window
对象
3.原型模式:通过 "复制" 创建新对象
原型模式的核心是 "基于已有对象(原型)复制创建新对象" ------ 不是从零开始定义新对象的属性和方法,而是直接 "拷贝" 一个现有对象(原型)的结构,再根据需要修改差异化内容。
前端中原型模式的本质:依托 JavaScript 原型链。
JavaScript 本身就是基于原型的语言,所有对象都有 __proto__
属性(指向其原型对象),这是原型模式在前端的 "天然实现"。
普通对象原型属性: 只有'proto'属性。
函数原型属性:proto、prototype属性。
prototype
:专属属性,只有函数有,用于 "当函数作为构造函数时,给新创建的实例提供原型"。
原型链顶端: Object.prototype.proto :指向null ;
前端典型场景:
1.Object.create()
2.Vue2 的数组方法重写:Vue2 为数组的push
、pop
等方法添加响应式逻辑,新数组会继承这些重写后的方法。
3.继承
工厂模式与原型模式区别:
工厂模式:
基于参数 / 规则 "全新创建" 对象;
核心目的:封装复杂的创建逻辑,让调用者无需关心对象构造细节。
原型模式
基于 "已有原型对象" 复制生成新对象
核心目的:复用已有对象的属性 / 方法,减少重复定义,支持继承扩展
4.观察者模式:"一对多" 的依赖通知机制
观察者模式(Observer Pattern)是一种 "一对多" 的依赖关系设计模式:
- 存在一个 "被观察者(Subject)" 和多个 "观察者(Observer)";
- 当被观察者的状态发生变化时,会自动通知所有依赖它的观察者,并触发观察者的更新逻辑;
- 核心是 "解耦被观察者和观察者"------ 双方无需知道彼此的具体实现,只需通过统一的接口通信
前端典型场景:
1.浏览器事件监听(最基础的观察者模式)
浏览器的 DOM 事件本质是观察者模式的实现:
- 被观察者:DOM 元素(如按钮);
- 观察者 :事件处理函数(
onclick
、onchange
等); - 流程:给元素绑定事件(订阅)→ 元素状态变化(如被点击)→ 自动执行所有绑定的事件处理函数(通知观察者)。
- 观察者模式的核心价值是 "状态变化自动同步"
2.状态管理库(Vuex/Pinia/Redux)
Vuex、Redux 等全局状态管理库的核心机制就是观察者模式:
- 被观察者 :Store 中的状态(如
state.user
、state.cart
); - 观察者:依赖该状态的组件;
- 流程 :组件订阅状态(
mapState
或useSelector
)→ 状态更新(commit
或dispatch
)→ 所有订阅该状态的组件自动重新渲染(收到通知更新)
3. 框架的响应式系统(Vue/React)
Vue 的响应式原理(数据驱动视图)和 React 的状态更新机制,底层都依赖观察者模式:
- Vue :数据对象(
data
)是被观察者,视图(DOM)和计算属性是观察者 ------ 数据变化时,Vue 自动触发依赖收集的观察者(视图重新渲染、计算属性重新计算)。 - React :
setState
触发状态更新时,组件树中依赖该状态的组件(观察者)会被重新渲染(收到通知执行更新)。
5.发布-订阅模式
发布 - 订阅模式是观察者模式的变体,核心是通过一个 "中间者(事件中心)" 实现 "发布者" 和 "订阅者" 的完全解耦 ------ 发布者不用知道谁在订阅,订阅者也不用知道谁在发布,双方仅通过事件中心传递消息,就像 "报社(发布者)→ 邮局(事件中心)→ 订报人(订阅者)" 的关系。
-
三大角色:
- 发布者(Publisher) :负责 "发布事件"(比如触发某个状态变化,如用户登录、数据更新),但不直接联系订阅者;
- 订阅者(Subscriber) :负责 "订阅事件"(比如关注 "用户登录" 事件),并定义事件触发时的 "回调逻辑"(比如登录后显示欢迎信息);
- 事件中心(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
拦截数组,只能逐个拦截索引(如 0
、1
),但存在两个致命问题:
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
时,数组会清空所有元素(即删除索引0
、1
、2
),但Object.defineProperty
只能知道length
被改成了0
,无法知道具体哪些元素被删除了。
对于响应式系统来说,需要知道 "哪些元素变化了" 才能精准通知依赖这些元素的视图。但 length
拦截只能知道 "长度变了",无法定位具体变化的元素,导致依赖这些元素的视图可能不会更新(比如某个视图依赖 arr[0]
,length=0
后 arr[0]
不存在了,但视图可能还显示旧值)。
2.Vue3 用 Proxy
直接代理整个对象,能拦截所有属性的读写(包括新增、删除、数组操作),是更彻底、更灵活的代理模式实现,这也是 Vue3 响应式比 Vue2 强大的核心原因之一。
总结
最后想强调:设计模式不是必须遵守的 "规则",而是解决问题的 "工具"。在实际开发中,我们不需要刻意追求 "用满所有模式",而是根据场景选择合适的工具:
- 需批量创建对象 → 工厂模式
- 需全局唯一实例 → 单例模式
- .....