第2章 2.2 响应式原理:Proxy 与 ref/reactive
🎯 为什么要学这个?
上一章「Composition API 基础」里,老王学会了用 setup() 函数组织代码,发现「逻辑可以拎出来放一起了」——但有个大问题:界面怎么知道数据变了?
想象这个场景:你点了个外卖,商家说「做好了」,但你的手机铃声没响、通知没弹——你就这么干等着,不知道外卖到了没。传统方式是你每隔 10 秒问一次「到了没?」累不累?
响应式的核心就是:数据变了,界面自动更新,你什么都不用做。
这章学完,你能:
- 理解 Vue3「数据变了,界面自动更新」背后的黑科技
- 搞懂 ref 和 reactive 到底在搞什么鬼
- 写出「数据一变,UI 自动刷新」的代码
🧱 基础:响应式原理是什么?
1. 先搞懂 Proxy(拦截器)
生活类比:你家小区的快递柜
快递员不直接把快递交给你,而是放到快递柜里。快递柜会「拦截」每一个快递,记录谁收了、什么时候收的。你拿快递的时候,柜子「知\n\n
\n\n
\n\n道」这个动作发生了。
在代码里,Proxy 就是这个快递柜——它拦截对对象的「读」和「写」,在中间偷偷做一些事。
Python 里没有真正的 Proxy,但我们可以用类来模拟这个拦截效果:
class ReactiveObject:
"""模拟 Vue3 的响应式对象"""
def __init__(self, data):
self._data = data
self._watchers = [] # 订阅者列表
def get(self, key):
print(f"📖 读取了属性: {key}")
return self._data[key]
def set(self, key, value):
print(f"✏️ 修改了属性: {key} = {value}")
self._data[key] = value
self._notify()
def _notify(self):
print(f"🔔 通知所有订阅者,数据变了!")
for watcher in self._watchers:
watcher()
def watch(self, callback):
"""订阅数据变化"""
self._watchers.append(callback)
# 使用
user = ReactiveObject({"name": "小明", "age": 18})
user.watch(lambda: print("😎 界面该更新了!"))
print(user.get("name")) # 读
user.set("age", 19) # 改 → 自动触发通知
输出:
📖 读取了属性: name
小明
✏️ 修改了属性: age = 19
🔔 通知所有订阅者,数据变了!
😎 界面该更新了!
看!修改 age 后,自动触发了通知,界面就知道该更新了。这就是「响应式」的核心。
2. ref 和 reactive:两种创建响应式数据的方式
Vue3 给了我们两个武器:
| 方式 | 适用场景 | 举例 |
|---|---|---|
ref() |
基础类型(字符串、数字、布尔) | ref(0)、ref("hello") |
reactive() |
对象/数组 | reactive({name: "小明"}) |
生活类比:
- ref 像给单个快递装追踪器(一个盒子)
- reactive 像给整个货架装追踪器(很多盒子)
Python 模拟:
# ref 的 Python 版模拟
class Ref:
"""ref 适用于单个值"""
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
print(f"🔄 ref 的 value 从 {self._value} 变成了 {new_value}")
self._value = new_value
# reactive 的 Python 版模拟
class Reactive:
"""reactive 适用于对象"""
def __init__(self, obj):
self._obj = obj
def __getattr__(self, key):
print(f"📖 读取 {key}")
return self._obj[key]
def __setattr__(self, key, value):
if key == "_obj":
super().__setattr__(key, value)
else:
print(f"✏️ 修改 {key} = {value}")
self._obj[key] = value
# 用起来
count = Ref(0)
count.value += 1
# 输出: 🔄 ref 的 value 从 0 变成了 1
info = Reactive({"name": "小明"})
info.name = "老王"
# 输出: ✏️ 修改 name = 老王
3. toRef 和 toRefs:解构不丢失响应式
痛点:你想从 reactive 对象里「拿出」一个属性单独用,但普通解构会丢失响应式。
# 普通解构 → 丢响应式 ❌
origin = {"name": "小明", "age": 18}
# 假设 origin 是 reactive 的,这里简化演示
name = origin["name"] # 普通赋值,name 变了 origin 不知道
toRef 的作用:创建一个「引用」,改了这个引用,原对象也跟着变。
class toRef:
"""把对象的某个属性变成响应式引用"""
def __init__(self, obj, key):
self._obj = obj
self._key = key
@property
def value(self):
return self._obj[self._key]
@value.setter
def value(self, new_val):
print(f"🔗 通过 toRef 修改 {self._key}")
self._obj[self._key] = new_val
# 用起来
user = {"name": "小明", "age": 18}
name_ref = toRef(user, "name")
print(name_ref.value) # 小明
name_ref.value = "老王"
print(user["name"]) # 老王 ✅ 两个地方同时变
🔥 实战:3 个小项目
项目 1:5 分钟 - 「数据一变,消息自动推送」(理解核心 API)
场景:做一个「监控配置文件」的小工具,配置文件一变,自动发消息通知。
import time
class ConfigWatcher:
"""配置文件监控器 - 演示响应式"""
def __init__(self, config):
self._config = config
self._callbacks = []
def get(self, key):
return self._config[key]
def set(self, key, value):
old = self._config[key]
self._config[key] = value
print(f"⚙️ 配置项 {key}: {old} → {value}")
self._notify(key, old, value)
def _notify(self, key, old, new):
for cb in self._callbacks:
cb(key, old, new)
def on_change(self, callback):
self._callbacks.append(callback)
# ✅ 完整可运行代码
def on_config_change(key, old, new):
print(f"📢 通知:{key} 变了!发邮件/推送/刷新界面...")
config = ConfigWatcher({"debug": False, "theme": "light", "timeout": 30})
config.on_change(on_config_change)
# 模拟配置变化
config.set("debug", True)
config.set("timeout", 60)
预期输出:
⚙️ 配置项 debug: False → True
📢 通知:debug 变了!发邮件/推送/刷新界面...
⚙️ 配置项 timeout: 30 → 60
📢 通知:timeout 变了!发邮件/推送/刷新界面...
解释:当配置变了,自动通知所有订阅者,界面就知道该刷新了。
项目 2:15 分钟 - 「待办清单」(从 JSON 读数据 + 响应式 CRUD)
场景:做一个命令行待办清单,数据存 JSON 文件,增删改查全支持。
import json
import os
class TodoList:
"""待办清单 - 响应式完整示例"""
def __init__(self, filename="todos.json"):
self._filename = filename
self._todos = self._load()
self._watchers = []
def _load(self):
if os.path.exists(self._filename):
with open(self._filename, "r", encoding="utf-8") as f:
return json.load(f)
return []
def _save(self):
with open(self._filename, "w", encoding="utf-8") as f:
json.dump(self._todos, f, ensure_ascii=False, indent=2)
self._notify("save", None, None)
def _notify(self, action, old, new):
for w in self._watchers:
w(action, old, new)
def watch(self, callback):
self._watchers.append(callback)
# CRUD 操作
def add(self, task):
self._todos.append({"id": len(self._todos)+1, "task": task, "done": False})
self._save()
print(f"✅ 添加了:{task}")
def done(self, task_id):
for t in self._todos:
if t["id"] == task_id:
old = t["done"]
t["done"] = True
self._save()
print(f"☑️ 完成了:{t['task']}")
return
print(f"❌ 没找到 id={task_id}")
def delete(self, task_id):
for i, t in enumerate(self._todos):
if t["id"] == task_id:
deleted = self._todos.pop(i)
self._save()
print(f"🗑️ 删除了:{deleted['task']}")
return
print(f"❌ 没找到 id={task_id}")
def list(self):
print("\n📋 待办清单:")
for t in self._todos:
status = "☑️" if t["done"] else "⬜"
print(f" {status} [{t['id']}] {t['task']}")
print()
# ✅ 完整可运行代码
def my_watcher(action, old, new):
print(f"🔔 监听:操作 {action} 触发了界面更新!")
todos = TodoList("todos.json")
todos.watch(my_watcher)
todos.add("买水果")
todos.add("写代码")
todos.list()
todos.done(1)
todos.list()
todos.delete(2)
todos.list()
预期输出:
✅ 添加了:买水果
🔔 监听:操作 save 触发了界面更新!
✅ 添加了:写代码
🔔 监听:操作 save 触发了界面更新!
📋 待办清单:
⬜ [1] 买水果
⬜ [2] 写代码
☑️ 完成了:买水果
🔔 监听:操作 save 触发了界面更新!
📋 待办清单:
☑️ [1] 买水果
⬜ [2] 写代码
🗑️ 删除了:写代码
🔔 监听:操作 save 触发了界面更新!
📋 待办清单:
☑️ [1] 买水果
解释:每次数据变化都自动触发 _notify,界面刷新就这么简单。
项目 3:15 分钟 - 「CSV 数据清洗工具」(组合 ref + reactive + toRef)
场景:做一个数据清洗工具,读取 CSV,对某列做统计,数据变了自动刷新报表。
import csv
from collections import Counter
class DataCleaner:
"""CSV 数据清洗 + 统计工具 - 响应式组合拳"""
def __init__(self):
self._raw_data = []
self._cleaned_data = []
self._stats = {}
self._watchers = []
def _notify(self):
for w in self._watchers:
w()
def watch(self, callback):
self._watchers.append(callback)
def load_csv(self, filepath):
"""加载 CSV"""
with open(filepath, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
self._raw_data = list(reader)
print(f"📂 加载了 {len(self._raw_data)} 条数据")
self._notify()
def clean(self, column, func):
"""清洗某列,应用 func"""
self._cleaned_data = [
{**row, column: func(row[column])} for row in self._raw_data
]
print(f"🧹 清洗完成,{column} 列已处理")
self._notify()
def stats(self, column):
"""统计某列的值分布"""
counter = Counter(row[column] for row in self._cleaned_data)
self._stats = dict(counter.most_common())
print(f"📊 统计完成:{column}")
self._notify()
return self._stats
def get_data(self):
return self._cleaned_data
def get_stats(self):
return self._stats
# ✅ 完整可运行代码(带模拟 CSV)
def my_reporter():
stats = cleaner.get_stats()
print(f"📺 报表刷新了!当前统计:{stats}\n")
# 先创建一个测试 CSV
with open("sales.csv", "w", encoding="utf-8") as f:
f.write("产品,销量,地区\n")
f.write("苹果,100,北京\n")
f.write("香蕉,80,上海\n")
f.write("苹果,120,广州\n")
f.write("香蕉,90,深圳\n")
cleaner = DataCleaner()
cleaner.watch(my_reporter)
cleaner.load_csv("sales.csv")
cleaner.clean("销量", lambda x: int(x) * 10) # 销量乘10
stats = cleaner.stats("产品")
print(f"🔍 最终结果:{stats}")
预期输出:
📂 加载了 4 条数据
📺 报表刷新了!当前统计:{}
🧹 清洗完成,销量 列已处理
📺 报表刷新了!当前统计:{}
📊 统计完成:产品
📺 报表刷新了!当前统计:{'苹果': 2, '香蕉': 2}
🔍 最终结果:{'苹果': 2, '香蕉': 2}
解释:数据变了 → 自动刷新报表,这就是「响应式」最真实的用处。
💪 进阶:常见坑 + 小技巧
坑 1:ref 的 value 要手动加(Python 模拟版已处理)
# ❌ 错误:直接修改,不走响应式
# count = Ref(0)
# count += 1 # 普通赋值,不触发响应式
# ✅ 正确:要改 .value
# count.value += 1
坑 2:reactive 深层拦截,浅层不会
# ❌ 错误:嵌套对象不响应
# obj = reactive({"user": {"name": "小明"}})
# obj.user = {"name": "老王"} # 这个会响应
# obj.user.name = "老王" # 这个不会响应(取决于实现)
# ✅ 正确:整体替换或用 deep reactive
坑 3:toRef 和 toRefs 的区别
# ❌ toRef:创建一个引用
# ✅ toRefs:把整个对象拆成多个 ref
坑 4:解构 reactive 会丢失响应式
# ❌ 错误
# state = reactive({"a": 1, "b": 2})
# a = state.a # 丢失响应式,变成普通值
# ✅ 正确:用 toRefs
# state = reactive({"a": 1, "b": 2})
# refs = toRefs(state)
# a_ref = refs.a # 仍然是响应式的
调试技巧:print 大法
class DebugReactive:
def __init__(self, data):
self._data = data
def __getattr__(self, key):
print(f"🔍 GET {key} = {self._data.get(key, 'NOT_FOUND')}")
return self._data[key]
def __setattr__(self, key, value):
if key == "_data":
super().__setattr__(key, value)
else:
print(f"🔍 SET {key} = {value}")
self._data[key] = value
✏️ 练习题
练习 1(1 分钟):抄改
- 输入:Ref(5) → .value += 3
- 预期输出:🔄 ref 的 value 从 5 变成了 8
- 提示:直接改初始值和增量
练习 2(2 分钟):加 if
- 输入:在项目 1 里加个 if,debug=True 时才打印通知
- 预期输出:关闭 debug 后,修改不打印通知
- 提示:在 _notify 里加判断
练习 3(3 分钟):新数据
- 输入:用项目 2 的方法处理 {"title": "会议", "done": False} 和 {"title": "学习", "done": True}
- 预期输出:列出两个待办,已完成一个
- 提示:创建新的 TodoList 实例添加这两条
练习 4(4 分钟):串项目 2+3
- 输入:把项目 2 的待办数据导出成 CSV,再用项目 3 加载清洗
- 预期输出:CSV 里的数据被加载并统计
- 提示:项目 2 加个导出 CSV 方法
练习 5(5 分钟):分析报错
- 输入:下面的代码为什么会报错?
class MyRef:
def __init__(self, v):
self.value = v # 这里直接赋值,不是响应式的!
r = MyRef(1)
r.value = 2
print(r.value)
- 预期输出:解释原因
- 提示:对比正确的 Ref 实现
作业:做一个「响应式记账本」
- 需求:命令行记账工具,收入支出记录,数据持久化到 JSON
- 功能点:
1.add(type, amount, note)添加记录
2.balance()查看余额
3.records()查看所有记录 - 加分项:
1. 数据变化时自动打印报表
2. 支持按月份筛选记录 - 验收标准:能跑起来 + 数据持久化 + 有响应式通知
- 提交:评论区贴代码
📚 总结
这章学了 3 件事:
1. Proxy 是拦截读写的「快递柜」,数据变了它知道
2. ref 管单个值,reactive 管对象,toRef/toRefs 处理引用
3. 响应式的核心:数据一变,自动通知,所有订阅者更新
延伸资源:
- Vue3 官方文档:响应式进阶(vuejs.org)
- 《Vue3 设计解密》系列视频
- 视频:「20 分钟搞懂响应式原理」
互动钩子:
你在项目里用过响应式数据吗?遇到过什么坑?评论区聊聊,老粉优先回复!
下章预告:
现在你学会了「数据变了界面自动更新」,但什么时候该更新?组件从创建到销毁经历了什么?下一章「生命周期钩子」给你答案……

评论(0)