Vue响应式系统与计算属性缓存:从原理到实现
Vue的响应式系统是其核心特性之一,它让开发者能够以"数据驱动" 的方式构建应用——当数据变化时,视图会自动更新。而计算属性的缓存机制则是响应式系统的重要优化,避免了不必要的重复计算。本文将深入底层,解析这两个机制的实现原理。
一、计算属性的缓存机制:如何做到"依赖不变则结果不变"
计算属性的缓存并非简单的"记忆化",而是与Vue的响应式系统深度耦合的设计。其核心逻辑是:* 只有当计算属性依赖的响应式数据发生变化时,才会重新计算;否则直接返回缓存的结果*。
1. 缓存机制的实现原理
计算属性的缓存依赖于两个关键角色:
- 计算属性Watcher:专门用于跟踪计算属性的依赖和缓存状态
- 脏值标记(dirty):标识计算属性是否需要重新计算
(1)初始化阶段:创建计算属性Watcher
当组件初始化计算属性时,Vue会创建一个特殊的Watcher
实例(称为"计算属性Watcher"),并执行计算属性的getter
函数:
// 伪代码:计算属性初始化
function createComputedGetter(key) {
return function computedGetter() {
// 获取计算属性对应的Watcher
const watcher = this._computedWatchers[key];
if (watcher.dirty) {
// 如果是脏的,执行getter重新计算
watcher.evaluate();
}
// 依赖收集:让当前渲染Watcher依赖计算属性Watcher
if (Dep.target) {
watcher.depend();
}
// 返回缓存的结果
return watcher.value;
};
}
计算属性Watcher有一个dirty
属性(默认true
),表示"需要重新计算"。首次访问时,dirty
为true
,会执行getter
并将结果缓存到 watcher.value
,然后将dirty
设为false
。
(2)依赖追踪:关联计算属性与源数据
计算属性执行getter
时,会访问源数据(如this.price
)。此时,源数据的getter
会触发依赖收集,将计算属性Watcher加入源数据的依赖列表( Dep
):
// 伪代码:源数据的getter(简化版)
function getter() {
// Dep.target指向当前活跃的Watcher(此处为计算属性Watcher)
if (Dep.target) {
// 将计算属性Watcher加入当前数据的Dep
dep.depend();
}
return value;
}
当源数据变化时,会通知其Dep
中的所有Watcher(包括计算属性Watcher),此时计算属性Watcher会将自己的dirty
设为true
,标记为" 需要重新计算"。
(3)缓存命中:依赖不变时直接返回缓存
当再次访问计算属性时:
- 若
dirty
为false
(依赖未变),直接返回watcher.value
(缓存) - 若
dirty
为true
(依赖已变),重新执行getter
计算,并更新缓存
2. 缓存与方法的本质区别
方法每次调用都会执行,而计算属性的缓存基于依赖变化而非调用次数。例如:
<template>
<!-- 计算属性:依赖不变时只执行1次 -->
<p>{{ fullName }}</p>
<!-- 方法:每次渲染都执行 -->
<p>{{ getFullName() }}</p>
</template>
即使模板重新渲染(如其他不相关数据变化),只要计算属性的依赖没变,就会直接返回缓存值,这是计算属性性能优势的核心原因。
二、Vue响应式系统:数据劫持与依赖收集的协同工作
Vue的响应式系统核心目标是:当数据变化时,自动触发依赖该数据的代码(如视图渲染、计算属性、watch回调)执行。这个过程通过" 数据劫持"和"依赖收集"实现。
1. 核心设计思想
响应式系统的设计遵循"观察者模式",但做了针对性优化:
- 数据劫持:拦截数据的读写操作,在读取时收集依赖,在修改时通知依赖更新
- 依赖收集:精确记录"哪些代码依赖了哪些数据",确保数据变化时只更新相关代码
2. 数据劫持:如何监听数据的读写
数据劫持是响应式系统的"感知层",负责检测数据的访问和修改。Vue2和Vue3采用了不同的实现方案。
(1)Vue2:基于Object.defineProperty
Vue2通过Object.defineProperty
重写对象的getter
和setter
,实现对属性读写的拦截:
// Vue2响应式处理(简化版)
function defineReactive(obj, key, value) {
// 递归处理嵌套对象
observe(value);
// 创建依赖容器(Dep)
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 读取数据时:收集依赖
if (Dep.target) {
dep.depend(); // 将当前Watcher加入Dep
}
return value;
},
set(newValue) {
if (newValue === value) return;
value = newValue;
observe(newValue); // 新值也需要响应式处理
// 修改数据时:通知依赖更新
dep.notify(); // 触发Dep中所有Watcher的更新
}
});
}
// 递归处理对象所有属性
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
局限性:
- 无法监听对象新增/删除的属性(需用
Vue.set
/Vue.delete
) - 无法监听数组通过索引/长度修改的变化(需重写数组方法如
push
、splice
)
(2)Vue3:基于Proxy
Vue3改用Proxy
实现数据劫持,解决了Vue2的诸多局限:
// Vue3响应式处理(简化版)
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 读取数据时:收集依赖
track(target, key);
// 递归处理嵌套对象(懒代理,访问时才处理)
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
if (oldValue === value) return true;
const result = Reflect.set(target, key, value, receiver);
// 修改数据时:通知依赖更新
trigger(target, key);
return result;
},
deleteProperty(target, key) {
const hadKey = Reflect.has(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
// 删除属性时:通知更新
trigger(target, key);
}
return result;
}
});
}
优势:
- 原生支持监听对象新增/删除属性
- 原生支持监听数组索引、长度变化
- 采用"懒代理"模式,只在访问嵌套对象时才递归处理,性能更优
3. 依赖收集:如何记录"谁依赖了数据"
依赖收集是响应式系统的"记忆层",负责记录"哪些代码依赖了哪些数据",核心角色是Dep
和Watcher
。
(1)Dep:依赖容器
每个响应式数据(或属性)都对应一个Dep
实例,用于存储依赖该数据的所有Watcher
:
// 伪代码:Dep类
class Dep {
constructor() {
this.subscribers = new Set(); // 存储依赖的Watcher(去重)
}
// 收集依赖:将Watcher加入容器
depend() {
if (Dep.target) {
this.subscribers.add(Dep.target);
}
}
// 通知更新:触发所有Watcher的更新
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}
// Dep.target:当前活跃的Watcher(全局变量,同一时间只有一个)
Dep.target = null;
(2)Watcher:观察者
Watcher
是"依赖代码"的封装(如模板渲染函数、计算属性、watch回调),当依赖的数据变化时,Watcher
会执行对应的代码:
// 伪代码:Watcher类
class Watcher {
constructor(fn, options) {
this.fn = fn; // 需要执行的代码(如渲染函数)
this.options = options;
this.dirty = options.lazy || false; // 计算属性用:标记是否需要重新计算
if (!options.lazy) {
this.run(); // 非懒加载的Watcher(如渲染Watcher)初始化时执行一次
}
}
// 执行依赖代码
run() {
Dep.target = this; // 将当前Watcher设为活跃
this.value = this.fn(); // 执行代码(会触发数据getter,收集依赖)
Dep.target = null; // 重置活跃Watcher
}
// 标记为脏(计算属性用)
update() {
if (this.options.lazy) {
this.dirty = true; // 计算属性:只标记脏,不立即执行
} else {
this.run(); // 普通Watcher:立即执行
}
}
// 计算属性重新计算
evaluate() {
this.run();
this.dirty = false; // 计算后标记为干净
}
}
(3)依赖收集的完整流程
以模板渲染为例,依赖收集的过程如下:
- 初始化渲染Watcher:组件挂载时,创建渲染Watcher,其
fn
为组件的渲染函数 - 执行渲染函数:
Watcher.run()
被调用,Dep.target
设为当前Watcher - 访问数据触发getter:渲染函数中访问
this.message
,触发message
的getter
- 收集依赖:
message
的Dep
调用depend()
,将当前渲染Watcher加入subscribers
- 数据变化触发setter:当
this.message = 'new'
时,触发message
的setter
- 通知更新:
message
的Dep
调用notify()
,所有依赖的Watcher(如渲染Watcher)执行update()
- 重新渲染:渲染Watcher的
run()
执行,重新调用渲染函数,视图更新
4. 不同类型的Watcher
响应式系统中存在多种Watcher
,各自承担不同职责:
- 渲染Watcher:每个组件一个,负责执行组件渲染函数
- 计算属性Watcher:每个计算属性一个,负责跟踪依赖并缓存结果(
lazy: true
) - 用户Watcher:由
watch
选项或watch
API创建,负责执行用户定义的回调
三、响应式系统的局限性及解决方法
尽管Vue的响应式系统已经很强大,但仍存在一些边缘情况需要特殊处理。
1. Vue2的局限性及解决方案
对象新增/删除属性不响应
原因:Object.defineProperty
只能拦截已有属性
解决:用Vue.set(obj, key, value)
或this.$set
新增属性;用Vue.delete
删除属性数组索引/长度修改不响应
原因:Object.defineProperty
难以高效监听数组索引变化
解决:Vue2重写了数组的7个方法(push
、pop
、splice
等),调用这些方法会触发更新;或用Vue.set(arr, index, value)
嵌套对象需递归处理
原因:defineReactive
需提前递归处理所有嵌套属性,性能损耗
解决:Vue3改用Proxy的"懒代理"模式优化
2. Vue3的局限性及解决方案
Proxy无法监听原始值(如字符串、数字)
原因:Proxy只能代理对象,不能代理原始值
解决:用ref
包装原始值(ref(0)
),ref
内部通过对象的value
属性实现响应式某些特殊对象代理受限
原因:Proxy对Map
、Set
等内置对象的代理需要特殊处理
解决:Vue3提供shallowRef
、customRef
等API,支持自定义响应式逻辑IE浏览器不支持Proxy
原因:Proxy是ES6特性,IE完全不支持
解决:如需兼容IE,需使用Vue2或通过Babel+polyfill(效果有限)
四、总结
Vue的响应式系统是"数据驱动"理念的完美体现,其核心是:
- 数据劫持:Vue2用
Object.defineProperty
,Vue3用Proxy
,拦截数据读写 - 依赖收集:通过
Dep
(依赖容器)和Watcher
(观察者)记录数据与依赖代码的关系 - 自动更新:数据变化时,
Dep
通知所有Watcher
执行更新逻辑
而计算属性的缓存机制则是响应式系统的精细化优化:通过dirty
标记和依赖追踪,确保只有当源数据变化时才重新计算,大幅提升性能。
理解这些底层原理,不仅能帮助我们写出更高效的Vue代码,还能让我们在遇到响应式相关问题时(如数据更新但视图不刷新),快速定位并解决问题。