一、引言:为什么需要深入响应式原理?
在 Vue3 的世界中,响应式系统是整个框架的核心引擎。相比 Vue2 基于 Object.defineProperty
的实现,Vue3 使用 Proxy
彻底重构了响应式系统,带来了显著的性能提升和更强大的功能。
理解响应式原理的底层实现,能让你:
- 优化性能:避免不必要的重新渲染和计算
- 规避陷阱:识别常见反模式(如循环依赖、无效更新)
- 提升心智模型:写出更符合响应式思维的代码
- 掌握高级特性:灵活运用响应式API解决复杂问题
本文将深入剖析 ref
和 computed
这两个最常用的响应式API的底层实现,揭示Vue3响应式系统的设计哲学。
二、前置知识:响应式系统的基石
1. 核心概念回顾
- 副作用(Effect) :任何会改变程序状态的操作(如DOM更新)
- 依赖收集(Track) :追踪当前副作用依赖的数据
- 触发更新(Trigger) :当数据变化时通知相关副作用重新执行
2. 底层思路解析
Vue3 响应式系统建立在现代JavaScript Proxy特性之上:
// Proxy 基础示例
const data = { count: 0 };
const proxy = new Proxy(data, {
get(target, key) {
track(target, key); // 依赖收集
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key); // 触发更新
return true;
}
});
依赖存储的核心数据结构是嵌套的Map集合:
// 全局依赖存储结构
const targetMap = new WeakMap();
// 依赖收集伪代码
function track(target, key) {
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);
}
这种 WeakMap → Map → Set
的三层结构能高效管理依赖关系,同时避免内存泄漏。
三、ref 的魔法:基础类型的响应式方案
1. 为什么需要 ref?
ref
解决了响应式系统中的几个关键问题:
- 基本类型值(number, string等)的响应式包装
- DOM元素引用的响应式管理
- 性能隔离(独立于大型reactive对象)
2. 源码实现解密
下面是简化版的 ref
实现:
class RefImpl {
constructor(value) {
this._rawValue = value; // 存储原始值
this._value = convert(value); // 值转换(对象转为reactive)
this.dep = undefined; // 依赖收集器
}
get value() {
trackRefValue(this); // 触发依赖收集
return this._value;
}
set value(newVal) {
// 使用 Object.is 进行严格比较
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = convert(newVal);
triggerRefValue(this); // 触发依赖更新
}
}
}
// 辅助函数
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
function hasChanged(value, oldValue) {
return !Object.is(value, oldValue);
}
3. 关键机制详解
依赖收集过程:
function trackRefValue(ref) {
if (activeEffect) {
trackEffects(ref.dep || (ref.dep = new Set()));
}
}
function trackEffects(dep) {
dep.add(activeEffect);
}
值变更检测:
自动解包原理:
在模板中使用时,Vue编译器会自动添加 .value
访问,开发者无需手动编写.
注意事项,参考官方文档:在模板中解包的注意事项
四、computed:惰性求值与缓存的艺术
1. 设计哲学解析
计算属性的核心价值在于:
- 惰性计算:只在需要时执行
- 结果缓存:依赖未变化时直接返回缓存值
- 依赖追踪:自动管理依赖关系
2. 源码架构解剖
class ComputedRefImpl {
constructor(getter) {
this._dirty = true; // 脏检查标志
this._value = undefined;
// 创建副作用函数
this.effect = new ReactiveEffect(
getter,
() => {
// 调度器:依赖变更时触发
if (!this._dirty) {
this._dirty = true; // 标记需要重新计算
triggerRefValue(this); // 触发依赖更新
}
}
);
}
get value() {
// 收集访问计算属性的副作用
trackRefValue(this);
// 当值需要更新时重新计算
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
}
// 简化的副作用类
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
}
run() {
activeEffect = this;
const result = this.fn();
activeEffect = undefined;
return result;
}
}
3. 核心特性实现
缓存机制工作原理:
依赖更新流程:
- 依赖项改变,触发调度器
- 设置
_dirty = true
(标记需要重新计算) - 通知所有依赖此计算属性的副作用
- 当副作用重新执行时,访问计算属性触发重新计算
五、响应式系统的协同作战
1. ref 与 computed 的联动
典型场景:ref → computed → 组件渲染
const count = ref(0);
const double = computed(() => count.value * 2);
// 依赖关系:
// count → double → 渲染函数
当 count.value
改变时:
- 触发 count 的 setter
- 通知 double 的调度器
- double 标记为 dirty
- 通知渲染函数重新执行
- 渲染函数访问 double.value 触发重新计算
2. 内存管理揭秘
Vue3 使用 WeakMap 避免内存泄漏的关键:
// 全局依赖存储
const targetMap = new WeakMap();
// 当 target 不再被引用时
// WeakMap 中的条目会被自动垃圾回收
依赖自动清除机制:
// 组件卸载时
function cleanup(effect) {
for (const dep of effect.deps) {
dep.delete(effect);
}
effect.deps.length = 0;
}
六、实战中的高级技巧
1. 性能优化策略
避免 computed 的过度计算:
// 反模式:每次访问都重新计算
const expensive = computed(() => {
return hugeArray.value.filter(...).map(...)
});
// 优化:添加条件判断
const filtered = computed(() => {
if (!shouldCompute.value) return cachedResult;
return hugeArray.value.filter(...);
});
合理使用 shallowRef:
// 大型对象优化
const bigData = shallowRef({ ... });
// 需要更新时
bigData.value = { ...bigData.value, updatedProp: 'new' };
2. 陷阱规避指南
避免 .value 丢失响应性:
// 错误:解构失去响应性
const { value: count } = ref(0);
// 正确:保持引用
const countRef = ref(0);
const count = countRef.value; // 仅用于读取
解决循环依赖问题:
const a = ref(1);
const b = computed(() => a.value + 1);
// 循环依赖导致死锁
a.value = b.value; // 错误!
// 解决方案:使用 nextTick
import { nextTick } from 'vue';
nextTick(() => {
a.value = b.value;
});
七、延伸思考:响应式系统的未来
1. Vue Reactivity 的独立性
Vue3 的响应式系统已解耦为独立库( @vue/reactivity
),可在任何JS环境中使用:
import { ref, computed } from '@vue/reactivity';
// 在非Vue项目中使用
const state = ref(0);
const double = computed(() => state.value * 2);
2. 响应式编程范式的扩展
- 状态机:基于响应式的状态管理
- 时间旅行:响应式数据+快照实现状态回溯
- 响应式UI:将UI视为数据的函数
3. 与其他框架对比
特性 | Vue3 Reactivity | MobX | SolidJS |
---|---|---|---|
实现原理 | Proxy | Proxy | 编译时转换 |
细粒度更新 | 中等 | 高 | 极高 |
学习曲线 | 平缓 | 中等 | 陡峭 |
包大小 | 轻量(6kb) | 中等(16kb) | 极小(3kb) |
八、总结:重新理解响应式
通过深入 ref
和 computed
的底层实现,我们揭示了Vue3响应式系统的核心设计:
- ref 通过类封装和值检测,解决了基本类型的响应式问题
- computed 利用脏检查机制和调度器,实现了惰性求值和缓存
- 依赖管理 基于WeakMap的三层结构,高效且内存安全
- 响应式协同 通过精妙的依赖链通知机制,实现高效更新
理解这些底层原理不仅能帮你写出更高效的Vue代码,还能培养响应式编程思维。我强烈建议你在实际项目中尝试调试响应式系统:
// 调试小技巧
import { ref, onRenderTracked, onRenderTriggered } from 'vue';
export default {
setup() {
const count = ref(0);
onRenderTracked((e) => {
console.log('依赖收集:', e);
});
onRenderTriggered((e) => {
console.log('依赖触发:', e);
});
return { count };
}
};
响应式系统是Vue的灵魂所在,深入理解它将使你从Vue的使用者转变为Vue的掌控者。现在,是时候打开Vue源码,开始你的探索之旅了!
九、手撕实现 ref & computed Demo
在线html运行:uutool.cn/html/
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>原生JS实现Vue的ref 和 Computed</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
color: #fff;
min-height: 100vh;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.container {
max-width: 800px;
width: 100%;
background: rgba(0, 0, 0, 0.7);
border-radius: 16px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
color: #fdbb2d;
text-shadow: 0 0 10px rgba(253, 187, 45, 0.5);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.8;
}
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.panel {
background: rgba(30, 30, 46, 0.8);
border-radius: 12px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.panel-title {
font-size: 1.5rem;
margin-bottom: 20px;
color: #fdbb2d;
display: flex;
align-items: center;
gap: 10px;
}
.panel-title i {
font-size: 1.8rem;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input {
width: 100%;
padding: 12px;
border-radius: 8px;
border: 2px solid #4e4e7c;
background: rgba(26, 26, 42, 0.7);
color: white;
font-size: 1.1rem;
transition: all 0.3s;
}
input:focus {
outline: none;
border-color: #fdbb2d;
box-shadow: 0 0 0 3px rgba(253, 187, 45, 0.3);
}
.result {
background: rgba(26, 26, 42, 0.7);
border-radius: 8px;
padding: 20px;
margin-top: 20px;
border-left: 4px solid #fdbb2d;
}
.result-title {
font-size: 1.1rem;
margin-bottom: 10px;
color: #fdbb2d;
}
.result-value {
font-size: 1.8rem;
font-weight: bold;
text-align: center;
padding: 10px;
}
.explanation {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
h3 {
font-size: 1.4rem;
margin-bottom: 15px;
color: #fdbb2d;
}
p {
line-height: 1.6;
margin-bottom: 15px;
}
.code-block {
background: rgba(26, 26, 42, 0.9);
border-radius: 8px;
padding: 15px;
font-family: monospace;
font-size: 1rem;
overflow-x: auto;
margin: 15px 0;
border-left: 4px solid #fdbb2d;
}
.highlight {
color: #fdbb2d;
font-weight: bold;
}
@media (max-width: 768px) {
.content {
grid-template-columns: 1fr;
}
h1 {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>原生JavaScript实现Vue的Computed属性</h1>
<p class="subtitle">响应式数据与依赖追踪的纯JavaScript实现</p>
</header>
<div class="content">
<div class="panel">
<h2 class="panel-title">计算属性演示</h2>
<div class="form-group">
<label for="price">商品单价</label>
<input type="number" id="price" value="25" />
</div>
<div class="form-group">
<label for="quantity">购买数量</label>
<input type="number" id="quantity" value="4" />
</div>
<div class="form-group">
<label for="discount">折扣 (%)</label>
<input type="number" id="discount" value="10" />
</div>
<div class="result">
<div class="result-title">总价(含税)</div>
<div id="totalPrice" class="result-value">$99.00</div>
</div>
</div>
<div class="panel">
<h2 class="panel-title">实现原理</h2>
<div class="explanation">
<h3>核心概念</h3>
<p>这个实现模拟了Vue的响应式系统:</p>
<ul>
<li><span class="highlight">响应式数据</span> - 通过getter/setter追踪数据访问</li>
<li><span class="highlight">依赖收集</span> - 自动追踪计算属性所依赖的数据</li>
<li><span class="highlight">计算缓存</span> - 只有依赖变化时才重新计算</li>
<li><span class="highlight">自动更新</span> - 依赖变化时自动更新计算结果</li>
</ul>
<h3>关键代码</h3>
<p>创建响应式数据:</p>
<div class="code-block">
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
// 依赖收集
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value // 触发更新 trigger(target, key); return true;
},
})
}
</div>
<p>创建计算属性:</p>
<div class="code-block">
let value
let dirty = true
const effect = () => {
dirty = true
}
return {
get value() {
if (dirty) {
activeEffect = effect
value = getter()
dirty = false
activeEffect = null
}
return value
},
}
}
</div>
</div>
</div>
</div>
</div>
<script>
// 全局变量用于依赖追踪
let activeEffect = null
const targetMap = new WeakMap()
// 依赖收集
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach((effect) => effect())
}
}
// 创建响应式对象
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) // 触发订阅更新
return true
},
})
}
// 创建计算属性
function computed(getter) {
let value
let dirty = true
const effect = () => {
dirty = true
}
return {
get value() {
if (dirty) {
// 设置当前活动effect
activeEffect = effect
// 计算新值
value = getter()
dirty = false
activeEffect = null
}
return value
},
}
}
// 初始化应用
document.addEventListener('DOMContentLoaded', () => {
// 创建响应式对象
const state = reactive({
price: 25,
quantity: 4,
discount: 10,
})
// 创建计算属性
const total = computed(() => {
const subtotal = state.price * state.quantity
const discountAmount = subtotal * (state.discount / 100)
const totalAfterDiscount = subtotal - discountAmount
// 添加10%的税
return totalAfterDiscount * 1.1
})
// 绑定输入事件
document.getElementById('price').addEventListener('input', (e) => {
state.price = Number(e.target.value)
updateUI()
})
document.getElementById('quantity').addEventListener('input', (e) => {
state.quantity = Number(e.target.value)
updateUI()
})
document.getElementById('discount').addEventListener('input', (e) => {
state.discount = Number(e.target.value)
updateUI()
})
// 更新UI
function updateUI() {
document.getElementById('price').value = state.price
document.getElementById('quantity').value = state.quantity
document.getElementById('discount').value = state.discount
// 访问计算属性会触发计算(如果dirty)
document.getElementById('totalPrice').textContent = `$${total.value.toFixed(2)}`
}
// 初始渲染
updateUI()
})
</script>
</body>
</html>