Appearance
第3.4节:computed:计算属性的懒执行与缓存原理
计算属性(computed)是Vue.js 3响应式系统中的一个重要特性,它通过懒执行和智能缓存机制,在保证数据一致性的同时实现了卓越的性能优化。本节将深入分析计算属性的实现原理,揭示其背后的精妙设计。
3.4.1 计算属性的核心概念
什么是计算属性
计算属性是基于其依赖的响应式数据进行计算的属性,具有以下特点:
- 懒执行:只有在被访问时才会执行计算函数
- 智能缓存:只有当依赖发生变化时才会重新计算
- 响应式:计算属性本身也是响应式的,可以被其他计算属性或副作用依赖
- 只读性:默认情况下计算属性是只读的(除非提供setter)
使用示例
typescript
// 只读计算属性
const count = ref(1)
const doubleCount = computed(() => count.value * 2)
// 可写计算属性
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
}
})3.4.2 ComputedRefImpl类:计算属性的核心实现
类的基本结构
typescript
export class ComputedRefImpl<T = any> implements Subscriber {
// 缓存的计算结果
_value: any = undefined
// 依赖管理器,管理订阅该计算属性的副作用
readonly dep: Dep = new Dep(this)
// 标识这是一个ref
readonly __v_isRef = true
// 是否为只读
readonly __v_isReadonly: boolean
// 依赖链表(该计算属性依赖的其他响应式数据)
deps?: Link = undefined
depsTail?: Link = undefined
// 状态标志
flags: EffectFlags = EffectFlags.DIRTY
// 全局版本号,用于快速路径优化
globalVersion: number = globalVersion - 1
// SSR标识
isSSR: boolean
// 批量更新链表
next?: Subscriber = undefined
// 向后兼容
effect: this = this
}构造函数
typescript
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined,
isSSR: boolean,
) {
// 如果没有setter,则为只读
this[ReactiveFlags.IS_READONLY] = !setter
this.isSSR = isSSR
}3.4.3 懒执行机制:按需计算
value getter:懒执行的入口
typescript
get value(): T {
// 1. 依赖收集:将当前计算属性注册为依赖
const link = __DEV__
? this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
: this.dep.track()
// 2. 刷新计算属性(懒执行的核心)
refreshComputed(this)
// 3. 同步版本号
if (link) {
link.version = this.dep.version
}
// 4. 返回缓存的值
return this._value
}refreshComputed:懒执行的核心逻辑
typescript
export function refreshComputed(computed: ComputedRefImpl): undefined {
// 1. 快速路径:如果正在追踪且不脏,直接返回
if (
computed.flags & EffectFlags.TRACKING &&
!(computed.flags & EffectFlags.DIRTY)
) {
return
}
// 2. 清除脏标记
computed.flags &= ~EffectFlags.DIRTY
// 3. 全局版本号快速路径:如果全局没有响应式变化,直接返回
if (computed.globalVersion === globalVersion) {
return
}
computed.globalVersion = globalVersion
// 4. 依赖检查:如果依赖没有变化,不需要重新计算
if (
!computed.isSSR &&
computed.flags & EffectFlags.EVALUATED &&
((!computed.deps && !(computed as any)._dirty) || !isDirty(computed))
) {
return
}
// 5. 开始计算
computed.flags |= EffectFlags.RUNNING
const dep = computed.dep
const prevSub = activeSub
const prevShouldTrack = shouldTrack
activeSub = computed
shouldTrack = true
try {
// 6. 准备依赖收集
prepareDeps(computed)
// 7. 执行计算函数
const value = computed.fn(computed._value)
// 8. 检查值是否变化,更新缓存
if (dep.version === 0 || hasChanged(value, computed._value)) {
computed.flags |= EffectFlags.EVALUATED
computed._value = value
dep.version++
}
} catch (err) {
dep.version++
throw err
} finally {
// 9. 恢复执行环境
activeSub = prevSub
shouldTrack = prevShouldTrack
// 10. 清理未使用的依赖
cleanupDeps(computed)
computed.flags &= ~EffectFlags.RUNNING
}
}3.4.4 缓存机制:智能的性能优化
多层缓存策略
Vue 3的计算属性采用了多层缓存策略:
1. 全局版本号缓存
typescript
// 全局版本号,每次响应式变化时递增
export let globalVersion = 0
// 在refreshComputed中的快速路径
if (computed.globalVersion === globalVersion) {
return // 全局没有任何响应式变化,直接返回缓存值
}2. 脏标记缓存
typescript
// 如果计算属性不脏且正在追踪,直接返回
if (
computed.flags & EffectFlags.TRACKING &&
!(computed.flags & EffectFlags.DIRTY)
) {
return
}3. 依赖版本号缓存
typescript
function isDirty(sub: Subscriber): boolean {
for (let link = sub.deps; link; link = link.nextDep) {
if (
link.dep.version !== link.version ||
(link.dep.computed &&
(refreshComputed(link.dep.computed) ||
link.dep.version !== link.version))
) {
return true
}
}
return false
}缓存失效机制
当依赖的响应式数据发生变化时,计算属性的缓存会失效:
typescript
// 在Dep.notify()中标记计算属性为脏
notify(): true | void {
this.flags |= EffectFlags.DIRTY
if (
!(this.flags & EffectFlags.NOTIFIED) &&
activeSub !== this
) {
batch(this, true)
return true
}
}3.4.5 依赖追踪:精确的依赖管理
依赖收集过程
计算属性既是依赖的消费者,也是依赖的提供者:
typescript
// 作为消费者:收集自己依赖的响应式数据
get value(): T {
// 当前计算属性成为activeSub
activeSub = computed
// 执行计算函数时,会触发依赖的getter,从而收集依赖
const value = computed.fn(computed._value)
// 恢复之前的activeSub
activeSub = prevSub
}
// 作为提供者:被其他副作用依赖
get value(): T {
// 将访问该计算属性的副作用注册为订阅者
this.dep.track()
return this._value
}依赖链表管理
计算属性使用双向链表管理其依赖关系:
typescript
// 依赖链表的头尾指针
deps?: Link = undefined
depsTail?: Link = undefined
// 在prepareDeps中标记所有依赖
function prepareDeps(sub: Subscriber) {
for (let link = sub.deps; link; link = link.nextDep) {
link.version = -1
link.prevActiveLink = link.dep.activeLink
link.dep.activeLink = link
}
}
// 在cleanupDeps中清理未使用的依赖
function cleanupDeps(sub: Subscriber) {
let head
let tail = sub.depsTail
let link = tail
while (link) {
const prev = link.prevDep
if (link.version === -1) {
// 移除未使用的依赖
removeSub(link)
removeDep(link)
} else {
head = link
}
link.dep.activeLink = link.prevActiveLink
link.prevActiveLink = undefined
link = prev
}
sub.deps = head
sub.depsTail = tail
}3.4.6 链式计算:计算属性依赖其他计算属性
链式依赖的处理
计算属性可以依赖其他计算属性,形成计算链:
typescript
const a = ref(1)
const b = computed(() => a.value * 2)
const c = computed(() => b.value + 1)
const d = computed(() => c.value * 3)递归刷新机制
在isDirty函数中,会递归检查依赖的计算属性:
typescript
function isDirty(sub: Subscriber): boolean {
for (let link = sub.deps; link; link = link.nextDep) {
if (
link.dep.version !== link.version ||
(link.dep.computed &&
// 递归刷新依赖的计算属性
(refreshComputed(link.dep.computed) ||
link.dep.version !== link.version))
) {
return true
}
}
return false
}避免重复计算
通过版本号机制避免在同一个更新周期内重复计算:
typescript
// 在refreshComputed中检查全局版本号
if (computed.globalVersion === globalVersion) {
return // 已经在当前更新周期内计算过了
}
computed.globalVersion = globalVersion3.4.7 可写计算属性:setter的实现
setter的基本实现
typescript
set value(newValue) {
if (this.setter) {
this.setter(newValue)
} else if (__DEV__) {
warn('Write operation failed: computed value is readonly')
}
}可写计算属性的创建
typescript
export function computed<T, S = T>(
options: WritableComputedOptions<T, S>,
debugOptions?: DebuggerOptions,
): WritableComputedRef<T, S> {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T> | undefined
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, isSSR)
return cRef as any
}3.4.8 状态标志系统:精确的状态管理
EffectFlags枚举
typescript
export enum EffectFlags {
ACTIVE = 1 << 0, // 激活状态
RUNNING = 1 << 1, // 正在运行
TRACKING = 1 << 2, // 正在追踪依赖
NOTIFIED = 1 << 3, // 已被通知更新
DIRTY = 1 << 4, // 需要重新计算
ALLOW_RECURSE = 1 << 5, // 允许递归
PAUSED = 1 << 6, // 暂停状态
EVALUATED = 1 << 7, // 已经计算过
}状态转换
typescript
// 初始状态:脏且需要计算
flags: EffectFlags = EffectFlags.DIRTY
// 开始计算时
computed.flags |= EffectFlags.RUNNING
// 计算完成后
computed.flags |= EffectFlags.EVALUATED
computed.flags &= ~EffectFlags.RUNNING
// 依赖变化时
this.flags |= EffectFlags.DIRTY3.4.9 批量更新机制
批量处理计算属性
typescript
let batchedComputed: Subscriber | undefined
export function batch(sub: Subscriber, isComputed = false): void {
sub.flags |= EffectFlags.NOTIFIED
if (isComputed) {
sub.next = batchedComputed
batchedComputed = sub
} else {
sub.next = batchedSub
batchedSub = sub
}
}批量执行
typescript
export function endBatch(): void {
if (--batchDepth > 0) {
return
}
// 先处理计算属性
if (batchedComputed) {
let e: Subscriber | undefined = batchedComputed
batchedComputed = undefined
while (e) {
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
e = next
}
}
// 再处理普通副作用
// ...
}3.4.10 SSR特殊处理
SSR模式下的计算属性
在SSR环境中,计算属性的行为有所不同:
typescript
// 在refreshComputed中的SSR处理
if (
!computed.isSSR &&
computed.flags & EffectFlags.EVALUATED &&
((!computed.deps && !(computed as any)._dirty) || !isDirty(computed))
) {
return
}SSR模式下的特点:
- 没有渲染副作用,计算属性没有订阅者
- 无法依赖脏检查,总是重新计算
- 依赖全局版本号进行缓存
3.4.11 性能优化策略
1. 多级缓存
- 全局版本号缓存:最快的缓存路径
- 脏标记缓存:避免不必要的依赖检查
- 依赖版本号缓存:精确的依赖变化检测
2. 懒执行
- 只有在访问时才执行计算
- 避免创建时的立即计算
- 减少不必要的计算开销
3. 智能依赖管理
- 动态依赖收集和清理
- 避免内存泄漏
- 精确的依赖追踪
4. 批量更新
- 将多个计算属性的更新合并
- 避免重复计算
- 保证更新的一致性
3.4.12 内存管理机制
依赖清理
typescript
function cleanupDeps(sub: Subscriber) {
// 清理未使用的依赖链接
// 避免内存泄漏
// 保持依赖图的准确性
}WeakMap的使用
计算属性通过WeakMap存储依赖关系,确保对象可以被正确回收:
typescript
// 在dep.ts中
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()3.4.13 调试支持
开发模式下的调试信息
typescript
// 调试选项
export interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
// 在computed函数中设置调试钩子
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack
cRef.onTrigger = debugOptions.onTrigger
}递归警告
typescript
// 开发模式下的递归检测
_warnRecursive?: boolean
// 在notify中检测递归
if (__DEV__) {
// TODO: 实现递归警告逻辑
}3.4.14 实际应用场景
1. 数据转换
typescript
const users = ref([
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 }
])
const userNames = computed(() =>
users.value.map(user => user.name)
)2. 复杂计算
typescript
const items = ref([1, 2, 3, 4, 5])
const statistics = computed(() => {
const sum = items.value.reduce((a, b) => a + b, 0)
const avg = sum / items.value.length
const max = Math.max(...items.value)
const min = Math.min(...items.value)
return { sum, avg, max, min }
})3. 条件渲染
typescript
const user = ref({ role: 'admin', permissions: ['read', 'write'] })
const canEdit = computed(() =>
user.value.role === 'admin' ||
user.value.permissions.includes('write')
)4. 表单验证
typescript
const email = ref('')
const password = ref('')
const isValidForm = computed(() => {
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)
const passwordValid = password.value.length >= 8
return emailValid && passwordValid
})3.4.15 最佳实践
1. 避免副作用
typescript
// ❌ 错误:在计算属性中产生副作用
const badComputed = computed(() => {
console.log('Computing...') // 副作用
return someValue.value * 2
})
// ✅ 正确:纯函数计算
const goodComputed = computed(() => {
return someValue.value * 2
})2. 合理使用可写计算属性
typescript
// ✅ 适合使用可写计算属性的场景
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
}
})3. 避免过深的计算链
typescript
// ❌ 过深的计算链可能影响性能
const a = computed(() => source.value * 2)
const b = computed(() => a.value + 1)
const c = computed(() => b.value * 3)
const d = computed(() => c.value - 1)
// ✅ 合并计算逻辑
const result = computed(() => {
const step1 = source.value * 2
const step2 = step1 + 1
const step3 = step2 * 3
return step3 - 1
})总结
Vue.js 3的计算属性系统是一个精心设计的性能优化机制,它通过以下关键技术实现了高效的响应式计算:
- 懒执行机制:只有在需要时才进行计算,避免不必要的开销
- 多级缓存策略:全局版本号、脏标记、依赖版本号的三重缓存
- 智能依赖管理:精确的依赖收集、清理和追踪
- 批量更新优化:合并多个计算属性的更新,保证一致性
- 链式计算支持:支持计算属性之间的依赖关系
- 内存管理:通过WeakMap和依赖清理避免内存泄漏
- 调试支持:提供丰富的调试信息和错误检测
这个系统不仅保证了计算属性的正确性和响应性,还通过多种优化策略实现了卓越的性能表现。理解这些机制对于编写高性能的Vue.js应用程序至关重要。
