computed 详解

computed() 创建计算属性——一种特殊的响应式数据,它的值由 getter 函数计算得出,当依赖的响应式数据变化时自动更新。

基本用法

tsx
import { ref, computed } from 'vitarx'

const count = ref(0)

// 创建计算属性
const double = computed(() => count.value * 2)

console.log(double.value) // 0
count.value = 3
console.log(double.value) // 6

计算属性和 ref 一样通过 .value 读取值,在 JSX 模板中会自动解包。

懒计算与脏标记

Vitarx 的 computed 采用懒计算策略:

  1. 首次访问 .value才执行 getter 函数
  2. 依赖变化时只标记脏(dirty),不立即重新计算
  3. 下次访问 .value 时,如果标记为脏才重新计算
tsx
import { ref, computed } from 'vitarx'

const count = ref(0)

// getter 不会立即执行
const double = computed(() => {
  console.log('getter 执行了')
  return count.value * 2
})

// 首次访问 .value 时才执行 getter
console.log(double.value) // 输出: "getter 执行了",然后输出 0

count.value = 5
// 此时 getter 还没有重新执行——只是标记为脏

// 再次访问 .value 时才重新计算
console.log(double.value) // 输出: "getter 执行了",然后输出 10

这种机制意味着:即使依赖频繁变化,只要你不读取计算属性的值,就不会产生计算开销。

getter 接收 oldValue 参数

getter 函数接收一个参数——上一次的计算结果(oldValue),第一次计算时为 undefined。你可以利用它做增量计算或条件判断:

tsx
import { ref, computed } from 'vitarx'

const items = ref([1, 2, 3])

// 利用 oldValue 做增量计算
const total = computed((oldValue) => {
  if (oldValue === undefined) {
    // 第一次计算
    return items.value.reduce((sum, n) => sum + n, 0)
  }
  // 后续可以基于 oldValue 做增量更新
  return items.value.reduce((sum, n) => sum + n, 0)
})

getter/setter 双向计算属性

默认情况下 computed 是只读的。你可以通过传入 { get, set } 对象创建可写的计算属性:

tsx
import { ref, computed, createApp } from 'vitarx'

function App() {
  const firstName = ref('张')
  const lastName = ref('三')

  // 可写的计算属性
  const fullName = computed({
    get: () => `#123;firstName.value}#123;lastName.value}`,
    set: (newValue) => {
      // 假设姓是第一个字,名是后面的字
      firstName.value = newValue[0]
      lastName.value = newValue.slice(1)
    }
  })

  return (
    <div>
      <p>姓名: {fullName}</p>
      <button onClick={() => (fullName.value = '李四')}>改名为李四</button>
    </div>
  )
}

createApp(App).mount('#app')

WARNING

如果 computed 没有提供 setter,尝试设置 .value 会在控制台输出警告,修改不会生效。

.dirty 属性

dirty 属性表示计算属性是否需要重新计算——当依赖变化时会标记为脏,但还没有重新计算:

tsx
import { ref, computed } from 'vitarx'

const count = ref(0)
const double = computed(() => count.value * 2)

console.log(double.dirty) // true(首次还未计算)

// 访问 .value 触发计算
console.log(double.value) // 0
console.log(double.dirty) // false(刚刚计算过)

// 修改依赖——标记为脏但不重新计算
count.value = 5
console.log(double.dirty) // true(需要重新计算)

// 访问 .value 时才重新计算
console.log(double.value) // 10
console.log(double.dirty) // false

.notify() 手动标记脏

notify() 方法用于手动将计算属性标记为脏,使下一次访问 .value 时触发重新计算。通常无需手动调用,除非你需要显式地通知计算属性其值已过时:

tsx
import { computed } from 'vitarx'

// 非响应式变量——computed 无法自动追踪它的变化
let multiplier = 2

const double = computed(() => 10 * multiplier)

console.log(double.value) // 20

// 修改非响应式变量——computed 不会感知到变化
multiplier = 3
console.log(double.value) // 仍然是 20

// 手动通知 computed 已脏,下次访问时会重新计算
double.notify()
console.log(double.value) // 30

notify() 返回当前实例,支持链式调用:

tsx
double.notify().value // 标记脏后立即访问,触发重新计算

INFO

notify() 不会立即重新计算,只是标记脏。只有在依赖确实发生了变化但你绕过了响应式系统(如修改了非响应式的外部数据)时,才需要手动调用。

.dispose() 手动销毁

dispose() 方法用于手动销毁计算属性,释放其依赖追踪和副作用资源:

tsx
import { ref, computed } from 'vitarx'

const count = ref(0)
const double = computed(() => count.value * 2)

console.log(double.value) // 0

// 手动销毁——不再追踪依赖
double.dispose()

// 之后 count 变化不会影响 double
count.value = 10
console.log(double.value) // 仍然是 0

INFO

通常不需要手动调用 dispose()——当计算属性所在的作用域被销毁时,会自动释放。只有在特殊场景下(如动态创建的计算属性需要提前释放)才需要手动调用。

isComputed()

isComputed() 判断一个值是否为 computed() 创建的计算属性实例:

tsx
import { ref, computed, isComputed } from 'vitarx'

const count = ref(0)
const double = computed(() => count.value * 2)

isComputed(double) // true
isComputed(count) // false
isComputed(42) // false

完整示例

下面是一个综合使用 computed 相关 API 的示例:

tsx
import { ref, computed, isComputed, createApp } from 'vitarx'

function App() {
  const price = ref(100)
  const quantity = ref(2)
  const discount = ref(0.9)

  // 只读计算属性
  const total = computed(() => {
    return Math.round(price.value * quantity.value * discount.value)
  })

  // getter/setter 双向计算属性
  const displayPrice = computed({
    get: () =>#123;price.value}`,
    set: (val) => {
      // 从字符串中提取数字
      const num = parseFloat(val.replace('¥', ''))
      if (!isNaN(num)) price.value = num
    }
  })

  // 使用 oldValue 参数
  const historyTotal = computed((oldValue) => {
    const newVal = price.value * quantity.value * discount.value
    return Math.round(newVal)
  })

  return (
    <div style={{ padding: '20px' }}>
      <h2>computed 详解示例</h2>

      <div style={{ marginTop: '16px' }}>
        <h3>只读计算属性</h3>
        <p>单价: {price}</p>
        <p>数量: {quantity}</p>
        <p>折扣: {discount}</p>
        <p>总价: {total}</p>
        <p>是否为脏: {String(total.dirty)}</p>
        <button onClick={() => quantity.value++}>增加数量</button>
      </div>

      <div style={{ marginTop: '16px' }}>
        <h3>可写计算属性</h3>
        <p>显示价格: {displayPrice}</p>
        <button onClick={() => (displayPrice.value = '¥200')}>设置为 ¥200</button>
      </div>

      <div style={{ marginTop: '16px' }}>
        <p>total 是否为 computed: {String(isComputed(total))}</p>
      </div>
    </div>
  )
}

createApp(App).mount('#app')

下一步