Vue 计算属性和侦听器详解

Vue 计算属性和侦听器详解

计算属性(computed)和侦听器(watch)是 Vue 响应式系统中两个核心概念,它们都以不同的方式响应数据变化。下面我将全面解析它们的原理、使用场景和最佳实践。

一、计算属性 (Computed)

1.1 基本概念

计算属性是基于它们的响应式依赖进行缓存的派生值,只有当依赖发生变化时才会重新计算

js 复制代码
const count = ref(0)
const doubleCount = computed(() => count.value * 2)

1.2 特点

  • 缓存机制:依赖不变时直接返回缓存值
  • 惰性求值只有被访问时才会计算
  • 响应式:自动追踪依赖关系

1.3 完整语法

js 复制代码
const userInfo = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

1.4 计算属性缓存原理

Vue 内部实现简化的依赖追踪机制:

js 复制代码
function computed(getter) {
  let value;
  let dirty = true; // 标记是否需要重新计算
  
  const runner = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true
      trigger(this, 'value') // 触发依赖更新
    }
  })
  
  return {
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      track(this, 'value') // 收集依赖
      return value
    }
  }
}

二、侦听器 (Watch)

2.1 基本概念

侦听器用于在数据变化时执行副作用操作,比计算属性更适合执行异步或开销较大的操作

js 复制代码
watch(count, (newVal, oldVal) => {
  console.log(`count变化: ${oldVal} -> ${newVal}`)
})

2.1.1 高级配置选项

js 复制代码
watch(source, callback, {
  immediate: true, // 立即执行
  deep: true,      // 深度监听
  flush: 'post',   // DOM更新后触发
  onTrack(e) {     // 调试依赖追踪
    debugger
  },
  onTrigger(e) {   // 调试触发更新
    debugger
  }
})

2.2 watch API 变体

2.2.1 侦听单个源
js 复制代码
// ref
watch(count, callback)

// getter 函数
watch(() => state.count, callback)

// 响应式对象属性
watch(() => obj.prop, callback)
2.2.2 侦听多个源
js 复制代码
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})
2.2.3 watchEffect

自动追踪依赖的即时回调:

js 复制代码
watchEffect(() => {
  console.log('count:', count.value)
})

特点

  • 自动收集依赖
  • 立即执行
  • 无需指定侦听源

2.3 watch中的陷阱 (避坑指南)

2.3.1. Ref

当使用 ref 包裹一个对象时,Vue 的响应式系统仍然能够监听到对象内部的变化,但有一些特定的行为需要注意。

1. ref 包裹对象的工作原理
js 复制代码
const objRef = ref({
  name: 'Alice',
  age: 25
})

实际上等价于:

js 复制代码
const objRef = ref(reactive({
  name: 'Alice',
  age: 25
}))

Vue 会自动用 reactive() 包裹对象值,所以:

  • 通过 .value 访问的是 reactive 代理对象
  • 对象内部的修改会被追踪
2. 监听对象内部变化
js 复制代码
watch(objRef, (newVal, oldVal) => {
  console.log('对象变化:', newVal)
}, { deep: true }) // Vue 3.4+ 可以省略 deep: true

可以监听到

  • 添加/删除属性
  • 修改嵌套属性
  • 数组变化
3.监听方式对比
  1. 监听整个 ref 对象
js 复制代码
// 方式1:监听整个ref(需要.value访问)
watch(() => objRef.value, (newVal) => {
  console.log('变化:', newVal)
}, { deep: true })

// 方式2:直接解包(Vue 3.3+)
watch(objRef, (newVal) => {
  console.log('变化:', newVal)
}, { deep: true })
  1. 监听特定属性
js 复制代码
// 监听特定属性(不需要deep)
watch(() => objRef.value.name, (newName) => {
  console.log('名字变化:', newName)
})
4.特殊情况处理
  1. 替换整个对象
js 复制代码
// 替换整个对象(会触发响应)
objRef.value = { name: 'Bob', age: 30 }

// 监听会触发,且能获取正确的oldVal
  1. 解构问题
js 复制代码
// 错误!失去响应性
const { name, age } = objRef.value;

// 正确保持响应性
const name = computed(() => objRef.value.name);
const { name } = toRefs(objRef.value);
2.3.2. Reactive

在 Vue 3 的响应式系统中,当你在 watch 中使用 reactive 对象时,会有一些特定的行为和注意事项。

js 复制代码
// 推荐 - 只侦听需要的属性
watch(() => state.importantProp, callback)

// 推荐 - 侦听多个属性
watch([() => state.a, () => state.b], ([a, b]) => {})

// 必要时才侦听整个对象
watch(state, callback, { deep: true }) // Vue 3.4+ 可省略 deep
1. 直接侦听整个 reactive 对象
js 复制代码
const state = reactive({ count: 0, user: { name: 'Alice' } })

// 侦听整个 reactive 对象
watch(state, (newVal, oldVal) => {
  console.log('state changed:', newVal)
})

行为特点

  • 任何嵌套属性的变化都会触发回调
  • newValoldVal 将是相同的引用(因为 reactive 对象是引用类型,所以 newVal === oldVal )
  • 需要 deep: true 才能正常工作(Vue 3.4+ 已默认启用)
2. 侦听 reactive 对象的特定属性
js 复制代码
watch(() => state.count, (newVal, oldVal) => {
  console.log('count changed:', newVal, oldVal)
})

行为特点

  • 只有特定属性变化才会触发
  • 可以正确获取 oldVal
  • 不需要 deep 选项
3.新旧值相同的问题

当侦听整个 reactive 对象时,newValoldVal 会是相同的:

js 复制代码
watch(state, (newVal, oldVal) => {
  console.log(newVal === oldVal) // true
})

原因:reactive 对象是引用类型,Vue 不会对其进行深拷贝

解决方案

  1. 侦听特定属性

  2. 手动深拷贝旧值:

    js 复制代码
    watch(() => ({ ...state }), (newVal, oldVal) => {
      console.log(newVal === oldVal) // false
    }, { deep: true })

三、计算属性 vs 侦听器

特性 计算属性 侦听器
目的 派生新数据 执行副作用
缓存 有缓存 无缓存
返回值 必须返回 不需要返回
异步 不支持 支持
初始化 惰性求值 可配置 immediate
依赖追踪 自动 显式指定
适用场景 模板渲染、数据转换 API调用、DOM操作

四、最佳实践

4.1 计算属性最佳实践

  1. 纯函数:避免副作用
  2. 简单计算:复杂逻辑考虑拆分
  3. 命名语义化 :如 fullNameisValid
  4. 避免修改依赖:保持单向数据流
js 复制代码
// 好的实践
const discountedPrice = computed(() => {
  return basePrice.value * (1 - discount.value)
})

// 不好的实践 - 有副作用
const badComputed = computed(() => {
  fetchData() // 副作用操作
  return ...
})

4.2 侦听器最佳实践

  1. 明确依赖:避免过度使用 deep
  2. 防抖节流:高频操作优化
  3. 清理副作用:返回清理函数
  4. 避免无限循环:注意修改侦听的数据
js 复制代码
// 带防抖的搜索
watch(searchQuery, debounce((query) => {
  fetchResults(query)
}, 500))

// 清理副作用示例
watch(data, (newVal) => {
  const timer = setInterval(() => {
    syncToServer(newVal)
  }, 1000)
  return () => clearInterval(timer)
})

五、性能优化

5.1 计算属性优化

  1. 减少依赖:只依赖必要的数据
  2. 避免复杂计算:大数组操作考虑预处理
  3. 使用 v-memo:配合计算属性优化渲染
js 复制代码
const bigList = computed(() => {
  // 使用 Map 优化查找性能
  const map = new Map()
  rawList.value.forEach(item => map.set(item.id, item))
  return map
})

5.2 侦听器优化

  1. 避免深度监听:明确指定嵌套路径
  2. 惰性监听 :使用 { lazy: true } 选项
  3. 分离监听器:不同逻辑分开监听
js 复制代码
// 优化前 - 深度监听整个对象
watch(obj, callback, { deep: true })

// 优化后 - 只监听需要的属性
watch(() => obj.importantProp, callback)

六、实际应用场景

6.1 计算属性典型场景

  1. 数据格式化
js 复制代码
const formattedDate = computed(() => {
  return new Date(date.value).toLocaleString()
})
  1. 过滤/排序列表
js 复制代码
const filteredUsers = computed(() => {
  return users.value.filter(u => u.active)
})
  1. 条件显示
js 复制代码
const showButton = computed(() => {
  return user.value.role === 'admin' && items.value.length > 0
})

6.2 侦听器典型场景

  1. API调用
js 复制代码
watch(route.params.id, (newId) => {
  fetchUser(newId)
})
  1. 表单验证
js 复制代码
watch(() => form.username, (newVal) => {
  validateUsername(newVal)
})
  1. 路由参数监听
js 复制代码
watch(() => route.params.id, (newId) => {
  fetchUserDetails(newId)
}, { immediate: true })
  1. 本地存储同步
js 复制代码
watch(todos, (newTodos) => {
  localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })

七、原理深入

7.1 计算属性实现机制

Vue 计算属性基于响应式系统和调度器实现:

  1. 首次访问:执行计算函数并缓存结果
  2. 依赖变化:标记缓存失效 (dirty = true)
  3. 再次访问:重新计算并更新缓存
  4. 无变化:直接返回缓存值

7.2 侦听器实现机制

Vue 侦听器基于 effect 和调度器:

  1. 初始化:创建 effect 并执行一次 (除非 lazy)
  2. 依赖变化:触发调度器
  3. 调度执行:根据 flush 时机执行回调
  4. 清理:组件卸载时自动清理

八、常见问题

8.1 计算属性 vs 方法

  • 计算属性:基于依赖缓存,适合派生数据
  • 方法:每次重新执行,适合事件处理
js 复制代码
// 计算属性 - 高效
const fullName = computed(() => `${firstName} ${lastName}`)

// 方法 - 每次重新计算
function getFullName() {
  return `${firstName} ${lastName}`
}

8.2 watch vs watchEffect

  • watch:需要显式指定源,更精确控制
  • watchEffect:自动收集依赖,更简洁
js 复制代码
// watch - 明确指定依赖
watch(() => state.count, (count) => {
  console.log(count)
})

// watchEffect - 自动追踪
watchEffect(() => {
  console.log(state.count)
})

8.3 为什么计算属性要有缓存?

  1. 性能优化:避免重复计算
  2. 一致性:保证多次访问同一值
  3. 避免副作用:防止意外多次执行

总结

计算属性和侦听器是 Vue 响应式系统的两大支柱:

  • 优先使用计算属性:用于派生数据和模板渲染
  • 合理使用侦听器:用于副作用操作和异步任务
  • 注意性能影响:避免不必要的重新计算和深度监听
  • 理解原理:有助于编写更高效的代码

掌握它们的区别和使用场景,可以显著提升 Vue 应用的性能和可维护性。

相关推荐
Hexene...4 小时前
【前端Vue】如何实现echarts图表根据父元素宽度自适应大小
前端·vue.js·echarts
初遇你时动了情4 小时前
腾讯地图 vue3 使用 封装 地图组件
javascript·vue.js·腾讯地图
华子w9089258594 小时前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
前端小趴菜057 小时前
React-forwardRef-useImperativeHandle
前端·vue.js·react.js
P7Dreamer7 小时前
Vue 3 + Element Plus 实现可定制的动态表格列配置组件
前端·vue.js
I'm写代码7 小时前
el-tree树形结构笔记
javascript·vue.js·笔记
斯~内克8 小时前
基于Vue.js和PDF-Lib的条形码生成与批量打印方案
前端·vue.js·pdf
sunbyte8 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ContentPlaceholder(背景占位)
前端·javascript·css·vue.js·tailwindcss
盏茶作酒299 小时前
打造自己的组件库(一)宏函数解析
前端·vue.js