框架中的设计模式深度解析
Vue 响应式系统本质是观察者模式、React 声明式 UI 本质是组合模式; 看透两大框架底层的设计模式,比死背 API 更能扛住原理类面试题。
一句话概括
Vue 和 React 不是凭空发明出来的——它们的底层架构,本身就是经典设计模式的”教科书式落地”。把框架当黑盒用久了,总会卡在一些说不清的”为什么”上:为什么解构会丢响应式?为什么 React 要我写 memo?答案都在设计模式里。
最核心的一句是:Vue 的响应式系统 = 观察者模式(数据变了,自动通知依赖它的地方重渲染);React 的声明式 UI = 组合模式(一切皆组件,组件套组件形成统一接口的树)。除此之外,虚拟 DOM 的 createElement/h 是工厂模式、diff 算法是策略模式、HOC/Context 是装饰器模式。
理解这些不是为了”背出框架用了什么模式”,而是让你从”会用框架”进阶到”懂框架”——能预测框架行为、能解释坑、也能在自己的组件库/SDK 里复用同一套思路。
核心知识点
1. Vue 响应式:观察者模式(Proxy 拦截 + 依赖收集/派发)
观察者模式的核心就八个字:读时收集,写时通知。Vue 3 用 Proxy 拦截对象的读写,把”谁在读这个属性”记下来(依赖收集),属性一变就挨个叫它们重跑(派发更新)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Vue 3 响应式核心(极简版)
type Effect = () => void;
const targetMap = new WeakMap<object, Map<PropertyKey, Set<Effect>>>();
let activeEffect: Effect | null = null;
function track(target: object, key: PropertyKey) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
dep.add(activeEffect);
}
function trigger(target: object, key: PropertyKey) {
targetMap.get(target)?.get(key)?.forEach(effect => effect());
}
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key); // 读时收集依赖
return Reflect.get(obj, key, receiver);
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver);
trigger(obj, key); // 写时通知更新
return result;
},
});
}
function watchEffect(fn: Effect) {
activeEffect = fn;
fn(); // 首次执行,触发依赖收集
activeEffect = null;
}
// 使用:数据是被观察者,effect 是观察者
const state = reactive({ count: 0 });
watchEffect(() => console.log('count =', state.count)); // 打印 0
state.count++; // 自动打印 1
对比一下”没观察者”的世界,你就知道它解决了什么:
1
2
3
4
5
6
7
// ❌ 普通对象:改了数据,谁依赖它根本不知道,视图不会自动更新
let state = { count: 0 };
function render() { document.body.textContent = state.count; }
state.count++; // render 不会自动跑,你得手动再调一次
// ✅ 观察者模式:框架替你记住"谁依赖了谁",数据一变自动重跑
// 上面 reactive + watchEffect 的效果
要点:Proxy 只是实现机制,观察者才是架构思想(被观察者 targetMap 持有观察者 Effect 的依赖关系)。Vue 2 用 Object.defineProperty 也是同一套思想,只是拦截能力更弱(拿不到新增属性、数组要 hack)。
2. React 声明式 UI:组合模式(统一接口的”部分-整体”)
React 官方原话是:”React has a powerful composition model, and we recommend using composition instead of inheritance.” 组合模式的核心:叶子节点和容器节点对外暴露完全一样的接口(接收 props、返回 JSX),所以可以无差别地嵌套成一棵树。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 用继承做弹窗变体:每加一种就新建子类,父类一改全体崩,还难复用
class BaseModal extends React.Component {
render() { return <div className="modal"><h2>{this.props.title}</h2>{this.renderBody()}</div>; }
renderBody() { return null; }
}
class ConfirmModal extends BaseModal { renderBody() { return <p>确定吗?</p>; } }
// ✅ 组合:容器只定义骨架,内容由 children / props 注入(React 官方推荐写法)
function Modal({ title, children, footer }: {
title: string; children: React.ReactNode; footer?: React.ReactNode;
}) {
return (
<div className="modal">
<header>{title}</header>
<main>{children}</main>
{footer && <footer>{footer}</footer>}
</div>
);
}
// 任意组件都是"统一接口"的节点,可无限嵌套组合
<Modal title="确认" footer={<Button>确定</Button>}>
<p>确定删除吗?</p>
</Modal>
Button、Card、List 长什么样不重要,组合模式只要求它们”接口一致”——Modal 不关心 children 里塞的是不是 List,它只负责把内容渲染出来。这正是”统一接口、递归嵌套”的树形结构。
3. 虚拟 DOM 工厂:createElement / h(工厂模式)
你写的 JSX 不是直接变成 DOM,而是先编译成 React.createElement(...) 调用,产出一个描述 UI 的普通对象(VNode)。这个”生产 VNode 的函数”就是工厂。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// JSX 语法糖的真相:都编译成对"虚拟节点工厂"的调用
// React
const vnode = React.createElement('div', { className: 'app' },
React.createElement('h1', null, 'Hi'));
// Vue
import { h } from 'vue';
const vnode = h('div', { class: 'app' }, [h('h1', 'Hi')]);
// 工厂模式的价值:把"怎么创建 VNode"的细节封装起来
// 开发者只描述"我要什么",工厂负责生成统一结构 { type, props, children }
interface VNode { type: string; props: Record<string, any>; children: any[]; }
function createElement(type: string, props: Record<string, any> | null, ...children: any[]): VNode {
return { type, props: props ?? {}, children };
}
每个节点都是 { type, props, children } 的统一结构——根节点和叶子节点长得一模一样,这正是组合模式在虚拟 DOM 层的体现。
4. diff / patch:按节点类型选算法(策略模式)
虚拟 DOM 比对不是一种算法打天下,而是看节点类型决定用哪套策略——类型不同就销毁重建,类型相同就复用并更新属性,列表变化就走 key 最小化移动。这就是策略模式:把”可互换的算法”封装好,运行时按条件选择。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type VNode = { type: string | Function; props: Record<string, any>; children: any[] };
// 策略模式:根据节点类型返回对应的比对策略
function getPatchStrategy(prev: VNode, next: VNode) {
if (prev.type !== next.type) return 'replace'; // 策略 A:类型不同 → 销毁重建
if (typeof prev.type === 'string') return 'updateProps'; // 策略 B:原生标签 → 复用 + 更新属性
return 'updateComponent'; // 策略 C:组件 → 递归 diff 子节点
}
// 调用方不关心细节,只管"给我比对"
function patch(prev: VNode, next: VNode) {
const strategy = getPatchStrategy(prev, next);
console.log('采用策略:', strategy);
}
5. HOC 与 Context:给组件”加能力”(装饰器模式)
装饰器模式 = 不改动原对象,外层包一层给它叠加新能力。React 的 HOC(高阶组件)和 Context 是典型的装饰器:把”鉴权、loading、埋点”等横切关注点从业务组件里抽出来。
1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 在每个页面组件里手动写 loading 判断,逻辑散落、无法复用
function UserList({ loading, users }: { loading: boolean; users: User[] }) {
if (loading) return <Spinner />;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ 抽成 HOC(装饰器):原组件零改动,需要时包一下就获得 loading 能力
function withLoading<P extends object>(Comp: React.ComponentType<P>) {
return (props: P & { loading: boolean }) =>
props.loading ? <Spinner /> : <Comp {...(props as P)} />;
}
const UserList = withLoading(UserListBase);
6. 一张表看清 Vue 与 React 的底层模式差异
| 维度 | Vue(观察者模式主导) | React(组合模式主导) |
|---|---|---|
| 数据变化 | Proxy 拦截,trigger 通知依赖的 effect | setState/useState 触发组件重渲染 |
| 更新粒度 | 精确到”属性级依赖”,只重跑相关 effect | 从触发组件开始递归 diff 整棵子树 |
| 变更检测 | 自动(读时收集依赖) | 手动(显式调用 setState) |
| 性能优化 | 通常无需手写 | 需 memo/useMemo/useCallback 裁剪 |
| 心智模型 | “改数据,UI 自动变” | “UI = f(state)” |
一句话:Vue 在数据层就知道谁要更新,React 在虚拟 DOM 层算出来谁要更新。所以 Vue 一般不用你手动优化,React 却要你主动 memo。
其实你每天都在用
ref/reactive数据一变,模板自动刷新:Proxy拦截读写 + 依赖收集,底层就是观察者模式computed/watchEffect自动重算:它是观察者模式里的”副作用(effect)”,依赖变了就被通知重跑- 写
<App><Header /><Content /></App>:组件层层嵌套,每个组件接口一致,就是组合模式 - 用 antd
<Modal>{children}</Modal>往里塞内容:容器留”洞”、调用方填内容,还是组合模式(等同 Vue 的 slot) - JSX 被编译成
React.createElement(...):你天天在调”虚拟节点工厂” - 给列表加
key让 diff 更准:diff 按”类型 + key”选最小移动策略,就是策略模式 withRouter或自定义 HOC 给页面加 props:原组件不动、外层包一层加能力,就是装饰器模式- Vue 的
v-model双向绑定:数据是观察者、输入框也是观察者,两边互相同步(双向观察)
常见误解(FAQ)
❌ 误区一:”Vue 是观察者、React 是组合,两者互不相关”
这是把”主导范式”说成了”唯一范式”。真相是两大框架都用了一堆模式:Vue 也有组合(slot、<slot> 和 children 同构),React 也有观察者思想的影子(比如 useSyncExternalStore 就是让外部 store 精确地通知组件)。区别只在谁是主心骨——Vue 以”数据变化自动通知”为主线,React 以”组件组合成树”为主线。面试说”主导”更准确。
❌ 误区二:”Proxy 拦截就是观察者模式本身”
机制 ≠ 模式。Proxy 只是 JavaScript 提供的一种”拦截属性读写”的能力(元编程),属于实现手段;观察者模式是架构层面的依赖关系(被观察者持有观察者的依赖集合,变化时统一通知)。Vue 用 Proxy 来实现观察者,但你用 Proxy 也能写出非观察者的东西。面试别把”用了 Proxy”等同于”用了观察者模式”。
❌ 误区三:”React 用组合模式,所以继承在 React 里完全没用”
接近事实,但要说准:React 不推荐用继承做 UI 复用,官方说”没遇到过需要用继承的场景”。但继承本身是语言特性,仍然可以用于非 UI 的逻辑抽象(比如多个类共享同一套方法时)。重点不是”禁用继承”,而是”UI 复用优先用组合(props/children)而非继承”。
❌ 误区四:”框架底层的设计模式只是面试背的,业务代码用不上”
正相反,懂了模式你才会写框架友好的代码。知道 Vue 是观察者,你就明白”解构会丢响应式”是因为脱离了 Proxy 的 get/set;知道 React 是组合,你就愿意用 children/slot 而不是堆继承;写组件库时用工厂创建节点、用装饰器加能力,都是同一套思路。模式是”看懂框架 + 写好业务”的通用语言。
一句话总结
Vue 用观察者模式把”数据变 → 自动通知”做到极致,React 用组合模式把”组件套组件”做成声明式范式,而工厂、策略、装饰器则是它们共同调用的”零件库”——看透这些,框架对你来说就不再是黑盒,而是一份你读得懂、用得顺的设计图纸。