响应式系统的核心是"数据变化驱动行为变化"。这一模块先建立范式认知,再进入框架实现细节。
什么是响应式编程
响应式编程(Reactive Programming)是一种面向**数据流(Data Stream)和变更传播(Change Propagation)**的声明式编程范式。它的核心思想是:
当数据源发生变化时,依赖该数据的所有目标自动更新,而无需手动干预。
命令式 vs 声明式
理解响应式的第一步是区分两种编程风格:
// ===== 命令式:描述"如何做" =====
// 每一步都手动操作 DOM
const count = 0;
const el = document.getElementById('count');
el.textContent = count;
document.getElementById('btn').addEventListener('click', () => {
// 手动修改状态
const newValue = parseInt(el.textContent) + 1;
// 手动同步到视图
el.textContent = newValue;
// 如果还有其他地方依赖这个值,也要手动更新
document.title = `计数: ${newValue}`;
});
// ===== 声明式:描述"做什么" =====
// 只需定义数据和视图的关系,框架自动处理同步
const state = reactive({ count: 0 });
function render() {
return h('div', [
h('span', { id: 'count' }, state.count),
h('button', { onClick: () => state.count++ }, '加一'),
]);
}
// state.count 变化时,render 自动重新执行
关键区别:
数据流
数据流是响应式系统的血管。理解数据如何流动,才能设计出可预测的状态架构。
单向数据流
┌─────────┐ ┌──────────┐ ┌──────┐
│ Action │ ──▶ │ Store │ ──▶ │ View │
│ (用户操作)│ │ (状态) │ ◀──│ │
└─────────┘ └──────────┘ └──────┘
▲ │
└──── Event ────┘
// 单向数据流的本质:
// 1. State 是唯一的真相来源
// 2. State 只能通过 Action 改变
// 3. View 是 State 的投影
// 4. 用户交互产生 Action
const store = {
state: { count: 0 },
dispatch(action) {
switch (action.type) {
case 'INCREMENT':
this.state.count += action.payload ?? 1;
break;
}
this.notify(); // 通知所有观察者
},
};
双向绑定
双向绑定是单向数据流的便捷语法糖:
View ──(用户输入)──▶ State
▲ │
└────(状态变更)──────┘
<!-- Vue 的 v-model 就是双向绑定的典型 -->
<input v-model="username" />
<!-- 等价于 -->
<input :value="username" @input="username = $event.target.value" />
双向绑定在简单表单场景非常高效,但过度使用会导致数据流向混乱。建议遵循原则:组件内部可用双向绑定,跨组件传递使用单向数据流。
观察者模式
观察者模式(Observer Pattern)是响应式系统的基石之一。
经典实现
class Subject { // 被观察者(主题)
#observers = new Set();
subscribe(observer) {
this.#observers.add(observer);
// 返回取消订阅函数
return () => this.#observers.delete(observer);
}
unsubscribe(observer) {
this.#observers.delete(observer);
}
notify(data) {
for (const observer of this.#observers) {
observer.update(data);
}
}
}
class Observer { // 观察者
constructor(name) {
this.name = name;
}
update(data) {
console.log(`[${this.name}] 收到通知:`, data);
}
}
// 使用
const subject = new Subject();
const sub1 = subject.subscribe(new Observer('A'));
const sub2 = subject.subscribe(new Observer('B'));
subject.notify({ value: 1 }); // A 和 B 都收到
sub1(); // 取消 A 的订阅
subject.notify({ value: 2 }); // 只有 B 收到
观察者在 Vue 中的体现
// Vue 组件本质上就是一个 Observer
export default {
data() {
return { message: 'hello' };
},
// 当 data.message 变化时,Vue 自动触发 re-render
// 这就是观察者模式的实际应用
};
发布订阅模式
发布订阅模式(Pub/Sub)是观察者模式的解耦版本——引入中间的**事件总线(Event Bus)**作为调度中心。
发布者 ──▶ 事件总线(Event Bus/EventEmitter) ──▶ 订阅者
│
├─▶ 订阅者 A
├─▶ 订阅者 B
└─▶ 订阅者 C
class EventEmitter {
#events = new Map();
on(event, listener) {
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
this.#events.get(event).push(listener);
return () => this.off(event, listener); // 返回取消函数
}
off(event, listener) {
const listeners = this.#events.get(event);
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) listeners.splice(index, 1);
}
}
emit(event, ...args) {
const listeners = this.#events.get(event);
if (listeners) {
for (const listener of [...listeners]) {
listener(...args); // 浅拷贝防止遍历时修改
}
}
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
// 使用示例
const bus = new EventEmitter();
// 订阅
const unsub = bus.on('user:login', (user) => {
console.log(`${user.name} 已登录`);
});
// 发布
bus.emit('user:login', { name: 'Alice' });
// 取消
unsub();
观察者 vs 发布订阅
响应式宣言
响应式系统应具备的核心特性,可以归纳为以下四个维度:
1. 响应性(Responsiveness)
系统必须能够及时感知并传播变化:
const a = reactive(1);
const b = computed(() => a * 2); // b = 2
a = 3; // b 应该立即变为 6,无需手动调用任何方法
2. 透明性(Transparency)
响应式行为对使用者应该是透明的——不需要关心底层如何工作:
const state = reactive({
user: { name: 'Alice', age: 25 },
});
// 直接读写即可,无需特殊 API
state.user.name = 'Bob'; // 自动触发更新
console.log(state.user.age); // 自动建立依赖
3. 可组合性(Composability)
响应式数据应该可以自由组合和派生:
const firstName = ref('Alice');
const lastName = ref('Smith');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
const greeting = computed(() => `Hello, ${fullName.value}!`);
// 修改源头会级联影响所有派生值
firstName.value = 'Bob';
// fullName 自动变为 "Bob Smith"
// greeting 自动变为 "Hello, Bob Smith!"
4. 可预测性(Predictability)
相同的输入始终产生相同的输出,且变更路径可追踪:
const source = ref(0);
const trace = [];
watchEffect(() => {
trace.push(source.value); // 完整记录每次变化
});
source.value = 1; // trace: [0, 1]
source.value = 2; // trace: [0, 1, 2]
响应式系统的核心循环
无论框架如何封装,每个响应式系统都在执行同一个最小闭环:
┌──────────────────────────────────────────────┐
│ │
│ ┌──────────┐ 收集依赖 ┌──────────┐ │
│ │ 读取数据 │ ──────────────▶ │ Dep/Track │ │
│ │ (Trigger) │ │ (收集阶段) │ │
│ └──────────┘ └──────────┘ │
│ ▲ │ │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ 触发通知 │ Watcher/ │ │
│ └────────────────── │ Effect │ │
│ 修改数据 (Set/Patch) │ (副作用) │ │
│ └──────────┘ │
│ │
└──────────────────────────────────────────────┘
三个关键动作:
追踪(Track):读取响应式数据时,记录谁在读
触发(Trigger):写入响应式数据时,通知所有被记录的依赖
调度(Schedule):决定何时以及如何执行副作用(同步/异步/批量)
小结
理解这些基础概念后,下一章将用原生 JavaScript 从零构建一个最小化的响应式系统。