第9章 9.1 Vue 3 源码:响应式实现
📖 这是系列文章「Vue3 从入门到精通」的第 41 章,继续更新!
🎯 开场 3 分钟:为什么要学这个?
上一章我们仿照掘金 Web 端写了一个带列表渲染、条件显示的小项目,用 v-for 和 v-if 解决了「数据怎么展示」的问题。
但你有没有想过:为什么 Vue 里的数据一变,页面就自动跟着变?这背后谁在盯着数据?谁在通知页面更新?
举个例子——就像你订外卖,手机上点一下,厨房就开始准备,快递小哥就出发了,整个流程是自动触发的,你不需要每隔一秒就打开 App 检查「我的外卖到哪了」。Vue 的响应式系统就是那个「自动盯着数据变化」的机制。
这一章,我们就来揭开这个黑盒子——手写一个简化版的 Vue 响应式系统,搞清楚:
ref和reactive到底是怎么「知道」数据变了?computed为什么能自动缓存、不重复计算?track和trigger这两个函数在源码里干什么活?
学完这章,你再看 Vu\n\n
\n\n
\n\ne 代码,心里会有一种「哦,原来是这样」的感觉,而不是蒙着眼背 API。
🧱 基础 25 分钟:核心概念(小白视角)
什么是响应式?先别想代码
生活类比:想象你家的智能空调,你设置了「室温高于 26 度就开冷气」。这个空调「响应」温度的变化,自动执行动作——你不需要盯着温度计手动开关。
Vue 的响应式就是:数据一改,所有用到这数据的地方自动更新。你改一个变量,页面上的 10 个地方同时变,这就是响应式的威力。
第一步:理解「依赖追踪」——谁在用这个数据?
Vue 响应式的核心思想就一句话:谁用了我的数据,谁就是我的「订阅者」,数据一变我就通知他。
举个例子,你手机里关注了某个博主,他发文章你就收到推送——你就是他的订阅者。Vue 里,模板里用了 {{ name }},那模板就是 name 的订阅者;computed 里用了 age,那这个 computed 就是 age 的订阅者。
用一个最简 Python 代码感受一下这个逻辑:
# 一个极简的响应式系统
class Reactive:
def __init__(self, initial_value):
self._value = initial_value
self._subscribers = [] # 订阅者列表
def subscribe(self, callback):
"""添加一个订阅者"""
self._subscribers.append(callback)
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
print(f"📢 检测到变化:{self._value} → {new_value}")
self._value = new_value
# 通知所有订阅者
for callback in self._subscribers:
callback(new_value)
# 用起来
name = Reactive("小明")
name.subscribe(lambda v: print(f"订阅者A收到通知:name变成{v}"))
name.subscribe(lambda v: print(f"订阅者B收到通知:name变成{v}"))
print("--- 第一次修改 ---")
name.value = "小红" # 触发通知,两个订阅者都会收到
运行结果:
--- 第一次修改 ---
📢 检测到变化:小明 → 小红
订阅者A收到通知:name变成小红
订阅者B收到通知:name变成小红
这行代码在干嘛:每次 name.value = xxx 时,所有订阅这个数据的地方都会收到通知——这就是响应式的雏形。
第二步:ref 和 reactive 的区别
Vue 3 里有两个核心 API:
ref:包装基本类型(字符串、数字、布尔),用.value访问reactive:包装对象,直接访问属性
用 Python 类比一下:
# ref 就像一个带锁的盒子,你要通过 .value 钥匙打开
# reactive 就像一个透明展示柜,里面的东西直接能看到
# ref 的使用方式(Python 模拟)
class Ref:
def __init__(self, value):
self._raw_value = value
@property
def value(self):
return self._raw_value
@value.setter
def value(self, new_val):
self._raw_value = new_val
# reactive 的使用方式(Python 模拟)
class Reactive:
def __init__(self, obj):
# 把对象的每个属性都变成响应式的
for key in obj:
setattr(self, key, obj[key])
user = Reactive({"name": "小明", "age": 18})
print(user.name) # 直接访问,不用 .value
print(user.age)
关键区别:
- ref 要写 .value,因为基本类型不能直接代理
- reactive 直接访问属性,因为对象可以被整体代理
第三步:track 和 trigger——Vue 源码里的两个关键函数
看 Vue 源码,你会发现两个高频函数:
# 模拟 Vue 源码中的 track 和 trigger
# 全局变量,模拟 Vue 的依赖收集桶
target_map = {} # {target: {key: [effect1, effect2]}}
def track(target, key):
"""
收集依赖
场景:某个 effect 执行时读取了 target.key
这时候就把这个 effect 记录下来,等 key 变的时候通知它
"""
effect = currently_running_effect # 当前正在执行的副作用
if effect:
# 把 effect 存到 target.key 的订阅列表里
if key not in target_map:
target_map[key] = []
if effect not in target_map[key]:
target_map[key].append(effect)
def trigger(target, key):
"""
触发更新
场景:target.key 被赋值了
这时候把所有订阅了这个 key 的 effect 都跑一遍
"""
if key in target_map:
for effect in target_map[key]:
effect() # 重新执行副作用函数
# 全局变量,标记当前正在运行的 effect
currently_running_effect = None
def effect(fn):
"""副作用函数包装器"""
def wrapper():
global currently_running_effect
currently_running_effect = fn
fn() # 执行时会触发 track
currently_running_effect = None
return wrapper
用人话解释:
- track = 登记:谁读了我的数据,我就把他记下来
- trigger = 通知:我的数据变了,把之前记下来的人全部叫醒
第四步:computed 为什么能懒求值?
computed(计算属性)有个特性:不是每次访问都重新计算,而是依赖不变就不算**。
用 Python 模拟一下:
class Computed:
def __init__(self, getter):
self._getter = getter
self._cached_value = None
self._dirty = True # 标记:是否需要重新计算
@property
def value(self):
if self._dirty:
print("🧮 执行计算...")
self._cached_value = self._getter()
self._dirty = False
return self._cached_value
def trigger(target, key):
"""触发时把相关的 computed 标记为 dirty"""
if key in target_map:
for effect in target_map[key]:
if hasattr(effect, '_dirty'):
effect._dirty = True # 下次访问就重新算
# 用起来
a = Ref(10)
b = Ref(5)
sum_computed = Computed(lambda: a.value + b.value)
print("第一次访问:", sum_computed.value) # 会打印 🧮 执行计算...
print("第二次访问:", sum_computed.value) # 直接返回缓存,不打印
print("--- 修改 a ---")
a.value = 20
print("第三次访问:", sum_computed.value) # 重新计算
运行结果:
🧮 执行计算...
第一次访问: 15
第二次访问: 15
--- 修改 a ---
🧮 执行计算...
第三次访问: 25
这行在干嘛:第一次访问 sum_computed.value 时执行计算,a 被修改后标记为「脏」,下次访问才重新算——这就是懒求值。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):实现一个最基本的响应式变量
目标:写一个 ref 函数,能创建响应式变量,变化时自动通知。
class Ref:
def __init__(self, value):
self._value = value
self._subscribers = []
def subscribe(self, fn):
self._subscribers.append(fn)
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
self._value = new_value
for fn in self._subscribers:
fn(new_value)
def ref(initial_value):
"""创建响应式变量"""
return Ref(initial_value)
# 跟着抄
count = ref(0)
# 订阅变化
count.subscribe(lambda v: print(f"订阅者A:count变成{v}"))
count.subscribe(lambda v: print(f"订阅者B:count变成{v}"))
# 测试
count.value = 10
count.value = 20
预期输出:
订阅者A:count变成10
订阅者B:count变成10
订阅者A:count变成20
订阅者B:count变成20
一句话解释:每次 count.value = xxx 时,所有订阅者都会收到通知——这就是响应式数据的基本原理。
项目 2(15 分钟):实现 reactive——响应式对象
目标:把一个普通对象变成响应式对象,访问或修改属性时都能被追踪。
class Reactive:
def __init__(self, obj):
# 用 __getattr__ 和 __setattr__ 拦截读写
self._data = obj
def __getattr__(self, key):
print(f"📖 读取属性: {key}")
return self._data.get(key)
def __setattr__(self, key, value):
if key == '_data':
# 特殊属性,直接设置
super().__setattr__(key, value)
else:
print(f"✏️ 修改属性: {key} = {value}")
self._data[key] = value
# 用起来
user = Reactive({"name": "小明", "age": 18})
print("--- 读取属性 ---")
print(user.name)
print(user.age)
print("--- 修改属性 ---")
user.name = "小红"
user.age = 20
print("--- 读取修改后的值 ---")
print(user.name, user.age)
预期输出:
--- 读取属性 ---
📖 读取属性: name
小明
📖 读取属性: age
18
--- 修改属性 ---
✏️ 修改属性: name = 小红
✏️ 修改属性: age = 20
--- 读取修改后的值 ---
📖 读取属性: name
小红
📖 读取属性: age
20
一句话解释:通过 __getattr__ 和 __setattr__ 拦截对象属性的读写,读的时候记录谁在用,写的时候通知所有订阅者——这就是 reactive 的核心原理。
项目 3(15 分钟):做一个「数据变了就自动发邮件提醒」的小工具
目标:综合运用响应式系统,实现一个「监控数据变化,自动执行回调」的工具。
from datetime import datetime
# 响应式容器
class ReactiveContainer:
def __init__(self, initial_data):
self._data = initial_data
self._watchers = {} # {key: [callback1, callback2]}
def watch(self, key, callback):
"""监控某个属性的变化"""
if key not in self._watchers:
self._watchers[key] = []
self._watchers[key].append(callback)
def get(self, key):
return self._data.get(key)
def set(self, key, value):
old_value = self._data.get(key)
self._data[key] = value
print(f"📝 [{datetime.now().strftime('%H:%M:%S')}] {key}: {old_value} → {value}")
# 触发所有监控这个属性的回调
if key in self._watchers:
for callback in self._watchers[key]:
callback(key, old_value, value)
# 创建一个配置容器
config = ReactiveContainer({
"smtp_server": "smtp.example.com",
"email_to": "admin@example.com",
"max_retries": 3
})
# 定义几个监控回调
def send_email_alert(key, old, new):
print(f" 📧 触发邮件告警:{key} 发生了变化!")
def log_change(key, old, new):
print(f" 📋 记录日志:变更详情 key={key}, old={old}, new={new}")
# 注册监控
config.watch("max_retries", send_email_alert)
config.watch("max_retries", log_change)
config.watch("email_to", send_email_alert)
# 模拟业务操作
print("=== 场景:管理员修改配置 ===")
config.set("max_retries", 5)
print()
config.set("email_to", "newadmin@example.com")
print()
config.set("smtp_server", "smtp.new.com")
预期输出:
=== 场景:管理员修改配置 ===
📝 [14:30:25] max_retries: 3 → 5
触发邮件告警:max_retries 发生了变化!
记录日志:变更详情 key=max_retries, old=3, new=5
📝 [14:30:25] email_to: admin@example.com → newadmin@example.com
触发邮件告警:email_to 发生了变化!
一句话解释:这个工具实现了「数据变了自动通知」的核心功能——配置改了就发邮件告警并记录日志,这就是 Vue 响应式系统的实际应用场景。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:忘记 reactive 返回的是Proxy对象
# ❌ 错误示例
user = reactive({"name": "小明"})
original = user # 直接赋值
original.name = "小红" # 以为复制了一个新对象
print(user.name) # 打印 "小红",原对象也被改了!
# ✅ 正确做法:用解构或 shallowReactive
from copy import deepcopy
user = reactive({"name": "小明"})
copy = deepcopy(user) # 深拷贝一个普通对象
copy.name = "小红"
print(user.name) # 还是 "小明"
坑 2:ref 和 reactive 混用时的解包问题
# ❌ 错误示例
const a = ref(1)
const obj = reactive({ a }) # 这里 a 不会自动解包
console.log(obj.a) # 输出 Ref 对象,不是 1
# ✅ 正确做法
const a = ref(1)
const obj = reactive({ a: a.value }) # 显式取值
console.log(obj.a) # 输出 1
坑 3:在 computed 里修改响应式数据
# ❌ 错误示例
computed(() => {
this.count++ // 不要在 computed 里修改数据!
return this.count
})
# ✅ 正确做法:用 watch 或 effect 处理副作用
watchEffect(() => {
console.log(this.count)
// 这里可以修改其他响应式数据
})
坑 4:直接赋值一个普通对象给 reactive
# ❌ 错误示例
const obj = { name: "小明" }
const state = reactive(obj)
const other = { name: "小红" }
state = reactive(other) // 这样写会失去响应式!
# ✅ 正确做法:用 ref 或解构
const state = ref({ name: "小明" })
state.value = { name: "小红" } // 整体替换
性能小贴士:computed vs watchEffect
# 如果只需要「根据响应式数据计算新值」,用 computed(自动缓存)
full_name = computed(lambda: first_name.value + " " + last_name.value)
# 如果需要「数据变化时执行副作用」,用 watchEffect(不缓存)
watchEffect(lambda: console.log(f"名字变了:{full_name.value}"))
调试技巧:打印依赖收集过程
# 在关键函数里加日志
def track(target, key):
effect = currently_running_effect
if effect:
print(f"🔍 track: {target}.{key} 被 effect 订阅")
target_map.setdefault(key, []).append(effect)
def trigger(target, key):
if key in target_map:
print(f"📢 trigger: 触发 {len(target_map[key])} 个订阅者")
for effect in target_map[key]:
effect()
✏️ 练习题 + 作业题
练习 1(2 分钟):改一个变量名
# 下面代码创建了一个响应式变量,请改成响应式用户分数
score = ref(0)
score.subscribe(lambda v: print(f"分数变化:{v}"))
score.value = 100
- 输入:运行上述代码
- 预期输出:分数变化:100
- 提示:把
score相关的变量名替换即可
练习 2(2 分钟):加一个条件判断
# 在项目1的基础上,只在分数 >= 60 时打印"及格了"
count = ref(0)
count.subscribe(lambda v: print(f"count变成{v}"))
count.value = 50
count.value = 70
- 输入:运行上述代码
- 预期输出:
count变成50不打印(或打印别的),count变成70打印 - 提示:在
subscribe的回调里加个if v >= 60:
练习 3(3 分钟):用项目 2 的方法处理新数据
# 用 ReactiveContainer 监控商品库存
# 要求:库存低于 10 时打印"库存不足"
- 输入:
config.set("stock", 5) - 预期输出:
📝 ... stock: X → 5+📋 库存不足提醒 - 提示:给
stock这个 key 注册一个 watch 回调
练习 4(3 分钟):把项目 2 和项目 3 串起来
# 用 ReactiveContainer 同时监控 max_retries 和 email_to
# 要求:任一变化都打印"配置已更新"
- 输入:分别设置两个属性
- 预期输出:两次都打印"配置已更新"
- 提示:用同一个回调函数监控两个 key
练习 5(5 分钟):分析报错
# 运行下面代码会报错,分析原因并修复
user = Reactive({"name": "小明"})
print(user.nickname) # 报错:KeyError
- 输入:运行上述代码
- 预期输出:打印
None而不是报错 - 提示:
__getattr__捕获 KeyError 时要返回默认值
作业:做一个「响应式配置管理器」
需求描述:做一个命令行小工具,能监控配置文件的修改,自动重新加载并通知订阅者。
功能点:
1. 用 ReactiveContainer 管理配置项
2. 支持 config.set(key, value) 修改配置
3. 支持 config.watch(key, callback) 注册回调
4. 模拟「配置文件热更新」场景
加分项:
1. 支持批量修改 config.update({"a": 1, "b": 2})
2. 打印出每次变更的「差异报告」
验收标准:
- 能跑起来
- 修改配置后正确触发回调
- 代码有中文注释
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
这一章我们学了 3 个核心点:
- 响应式的本质:谁用了数据,谁就是订阅者;数据一变,全部叫醒
track和trigger:一个登记依赖,一个触发更新computed的懒求值:依赖没变就不重新算,省性能
延伸学习资源:
- Vue 3 官方响应式文档 — 英文原版,建议配合本文学
- 《Vue 3 Design and Implementation》— 森尼学长写的源码解读,很详细
- Vue Core 源码仓库 — 进阶玩家可以直接看
packages/reactivity/src目录
互动钩子:你在项目中有没有遇到过「数据变了页面没更新」的坑?是怎么解决的?评论区聊聊,老粉优先回复!
📖 第 42 章预告:学会了响应式原理,下一章我们要深入 Vue 源码,看看一个组件是怎么「出生」的——从
createApp到mount,页面上的 div 是怎么一步步被造出来的……

评论(0)