Skip to content

第3.4节:computed:计算属性的懒执行与缓存原理

计算属性(computed)是Vue.js 3响应式系统中的一个重要特性,它通过懒执行和智能缓存机制,在保证数据一致性的同时实现了卓越的性能优化。本节将深入分析计算属性的实现原理,揭示其背后的精妙设计。

3.4.1 计算属性的核心概念

什么是计算属性

计算属性是基于其依赖的响应式数据进行计算的属性,具有以下特点:

  1. 懒执行:只有在被访问时才会执行计算函数
  2. 智能缓存:只有当依赖发生变化时才会重新计算
  3. 响应式:计算属性本身也是响应式的,可以被其他计算属性或副作用依赖
  4. 只读性:默认情况下计算属性是只读的(除非提供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 = globalVersion

3.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.DIRTY

3.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模式下的特点:

  1. 没有渲染副作用,计算属性没有订阅者
  2. 无法依赖脏检查,总是重新计算
  3. 依赖全局版本号进行缓存

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的计算属性系统是一个精心设计的性能优化机制,它通过以下关键技术实现了高效的响应式计算:

  1. 懒执行机制:只有在需要时才进行计算,避免不必要的开销
  2. 多级缓存策略:全局版本号、脏标记、依赖版本号的三重缓存
  3. 智能依赖管理:精确的依赖收集、清理和追踪
  4. 批量更新优化:合并多个计算属性的更新,保证一致性
  5. 链式计算支持:支持计算属性之间的依赖关系
  6. 内存管理:通过WeakMap和依赖清理避免内存泄漏
  7. 调试支持:提供丰富的调试信息和错误检测

这个系统不仅保证了计算属性的正确性和响应性,还通过多种优化策略实现了卓越的性能表现。理解这些机制对于编写高性能的Vue.js应用程序至关重要。


微信公众号二维码