Vue中的computed与watch底层实现原理最佳实践

 更新时间:2026年01月21日 16:16:40   作者:LYFlied  
本文介绍Vue中computed和watch的底层实现原理,对比了核心概念、设计差异以及使用场景,通过分析computed的初始化、依赖追踪、缓存机制,以及watch的初始化、深度监听、异步更新,本文提供了一个全面的对比总结,并给出了性能优化和最佳实践建议,感兴趣的朋友一起看看吧

Vue的computed与watch底层实现原理

1. 核心概念与设计差异

1.1 设计哲学对比

计算属性 (computed)

  • 目的:声明式地描述一个派生值
  • 特性:基于依赖缓存,惰性求值,响应式依赖追踪
  • 使用场景:模板中的复杂表达式、格式化数据、计算派生状态

侦听器 (watch)

  • 目的:响应式地执行副作用
  • 特性:主动观察,可执行异步操作,获取新旧值
  • 使用场景:数据变化时执行异步请求、执行复杂逻辑、调试数据变化

1.2 核心差异总结

// 计算属性 - 声明式、缓存、同步
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}
// 侦听器 - 命令式、主动、可异步
watch: {
  firstName(newVal, oldVal) {
    // 执行副作用
    this.fetchUserData(newVal)
  }
}

2. computed底层实现原理

2.1 初始化过程

2.1.1 定义阶段
// Vue内部处理computed选项
function initComputed(vm, computed) {
  const watchers = (vm._computedWatchers = Object.create(null))
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 为每个计算属性创建专门的Watcher
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      { lazy: true }  // 关键:标记为惰性求值
    )
    // 将计算属性代理到Vue实例
    defineComputed(vm, key, userDef)
  }
}
// 定义计算属性的getter/setter
function defineComputed(target, key, userDef) {
  // 支持自定义setter
  const setter = userDef.set || noop
  Object.defineProperty(target, key, {
    get: createComputedGetter(key),
    set: setter,
    enumerable: true,
    configurable: true
  })
}
2.1.2 惰性求值实现
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 关键:只有当dirty为true时才重新计算
      if (watcher.dirty) {
        watcher.evaluate()  // 执行计算,并标记dirty为false
      }
      // 依赖收集:将当前渲染Watcher添加到计算属性的依赖中
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

2.2 响应式依赖追踪机制

2.2.1 依赖收集过程
// Watcher类的关键方法
class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this.lazy = !!options.lazy  // 是否为计算属性Watcher
    this.dirty = this.lazy      // 惰性求值标记
    // 计算属性Watcher的getter是计算函数
    this.getter = expOrFn
    // 初始值
    this.value = this.lazy ? undefined : this.get()
  }
  get() {
    // 将当前Watcher推入栈顶
    pushTarget(this)
    let value
    try {
      // 执行getter,触发依赖收集
      value = this.getter.call(this.vm, this.vm)
    } finally {
      // 恢复之前的Watcher
      popTarget()
    }
    return value
  }
  evaluate() {
    // 计算属性的求值方法
    this.value = this.get()
    this.dirty = false  // 标记为已计算
  }
  depend() {
    // 让依赖自己的Watcher收集自己作为依赖
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
  update() {
    // 计算属性的更新策略
    if (this.lazy) {
      this.dirty = true  // 仅标记为脏,不立即重新计算
    } else {
      // 普通Watcher的更新逻辑...
    }
  }
}
2.2.2 依赖关系图
模板渲染Watcher
       ↑
       | depend()
计算属性Watcher (lazy: true, dirty: false)
       ↑
       | 在evaluate()中收集
依赖的响应式数据 (firstName, lastName)

2.3 缓存机制与性能优化

2.3.1 缓存实现
// 计算属性的缓存生命周期
const computedWatcher = new Watcher(vm, computedFn, null, { lazy: true })
// 第一次访问:计算并缓存
computedWatcher.dirty = true → evaluate() → 计算结果 → dirty = false
// 第二次访问(依赖未变):直接返回缓存
computedWatcher.dirty = false → 返回缓存值
// 依赖变化时:标记为脏
firstName改变 → computedWatcher.update() → dirty = true
// 再次访问时重新计算
computedWatcher.dirty = true → evaluate() → 重新计算 → dirty = false
2.3.2 多级计算属性依赖
computed: {
  // 计算属性之间可以相互依赖
  fullName() {
    return this.firstName + ' ' + this.lastName
  },
  greeting() {
    // 依赖另一个计算属性
    return 'Hello, ' + this.fullName
  }
}
// 依赖链:
// greeting → fullName → (firstName, lastName)

3. watch底层实现原理

3.1 初始化与Watcher创建

3.1.1 初始化过程
function initWatch(vm, watch) {
  for (const key in watch) {
    const handler = watch[key]
    // 支持数组形式的多个handler
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
function createWatcher(vm, expOrFn, handler, options) {
  // 处理对象形式的handler
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 支持字符串方法名
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 调用$watch API
  return vm.$watch(expOrFn, handler, options)
}
3.1.2 $watch API实现
Vue.prototype.$watch = function(expOrFn, cb, options) {
  const vm = this
  // 规范化参数
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true  // 标记为用户watcher
  // 创建用户Watcher
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 立即执行选项
  if (options.immediate) {
    cb.call(vm, watcher.value)
  }
  // 返回取消监听函数
  return function unwatchFn() {
    watcher.teardown()
  }
}

3.2 深度监听实现

3.2.1 deep选项的实现
class Watcher {
  constructor(vm, expOrFn, cb, options) {
    // ...
    this.deep = !!options.deep  // 深度监听标记
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 解析表达式为getter函数
      this.getter = parsePath(expOrFn)
    }
    this.value = this.get()
  }
  get() {
    pushTarget(this)
    let value
    try {
      value = this.getter.call(vm, vm)
    } finally {
      // 深度监听:递归遍历对象
      if (this.deep) {
        traverse(value)  // 遍历对象所有属性,触发它们的getter
      }
      popTarget()
    }
    return value
  }
}
3.2.2 traverse递归遍历
// 简化的traverse实现
function traverse(val) {
  const seenObjects = new Set()
  function _traverse(val) {
    // 跳过非对象、VNode和冻结对象
    if (!val || typeof val !== 'object' || Object.isFrozen(val)) {
      return
    }
    // 避免循环引用
    if (val.__ob__) {
      const depId = val.__ob__.dep.id
      if (seenObjects.has(depId)) {
        return
      }
      seenObjects.add(depId)
    }
    // 递归遍历数组和对象
    if (Array.isArray(val)) {
      for (let i = 0; i < val.length; i++) {
        _traverse(val[i])
      }
    } else {
      const keys = Object.keys(val)
      for (let i = 0; i < keys.length; i++) {
        _traverse(val[keys[i]])
      }
    }
  }
  _traverse(val)
}

3.3 异步更新与防抖控制

3.3.1 sync选项控制同步性
class Watcher {
  update() {
    if (this.sync) {
      // 同步执行
      this.run()
    } else if (this.lazy) {
      // 计算属性:仅标记为脏
      this.dirty = true
    } else {
      // 默认:推入异步队列
      queueWatcher(this)
    }
  }
  run() {
    const value = this.get()
    // 对于user watcher,调用回调并传入新旧值
    if (this.user) {
      try {
        this.cb.call(this.vm, value, this.value)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      this.cb.call(this.vm, value, this.value)
    }
    this.value = value
  }
}
3.3.2 支持异步回调
// watch示例:支持异步操作
watch: {
  searchQuery: {
    handler(newVal, oldVal) {
      // 可以执行异步操作
      this.fetchResults(newVal)
    },
    immediate: true,
    deep: false
  }
}
// 底层实现支持异步回调
const watcher = new Watcher(vm, 'searchQuery', function(newVal, oldVal) {
  // 回调函数可以是异步的
  setTimeout(() => {
    console.log('Async update:', newVal)
  }, 0)
}, { user: true })

4. 核心差异的详细对比

4.1 设计模式差异

计算属性(声明式/响应式)

// 底层:依赖追踪 + 惰性求值 + 缓存
// 类似:React的useMemo + 自动依赖追踪
computed: {
  // 声明"什么值",由Vue负责"如何计算"
  discountedPrice() {
    return this.price * (1 - this.discount)
  }
}

侦听器(命令式/副作用)

// 底层:观察者模式 + 主动执行
// 类似:React的useEffect
watch: {
  // 声明"当什么变化时,执行什么操作"
  price(newPrice) {
    // 执行副作用
    this.logPriceChange(newPrice)
    this.updateChart(newPrice)
  }
}

4.2 执行时机差异

4.2.1 计算属性执行时机
// 场景1:模板中使用计算属性
<template>
  <div>{{ discountedPrice }}</div>
</template>
// 执行流程:
// 1. 模板编译时发现discountedPrice
// 2. 触发discountedPrice的getter
// 3. 如果dirty=true,执行计算函数
// 4. 收集依赖(price, discount)
// 5. 缓存计算结果
4.2.2 侦听器执行时机
// 场景:监听price变化
watch: {
  price(newVal, oldVal) {
    // 执行流程:
    // 1. price被修改,触发setter
    // 2. price的dep通知所有watcher
    // 3. 找到对应的user watcher
    // 4. 推入异步更新队列
    // 5. 下一个tick执行回调
    console.log(`Price changed from ${oldVal} to ${newVal}`)
  }
}

4.3 依赖处理差异

4.3.1 计算属性的自动依赖收集
computed: {
  userInfo() {
    // Vue自动追踪以下依赖:
    // this.user.name, this.user.age, this.isVIP
    return {
      name: this.user.name,
      age: this.user.age,
      level: this.isVIP ? 'VIP' : 'Normal'
    }
  }
}
// 依赖关系自动建立,无需手动声明
4.3.2 侦听器的显式依赖声明
watch: {
  // 需要明确指定要监听的数据路径
  'user.name': function(newName) {
    // 只监听user.name的变化
  },
  // 或者监听整个对象(需要deep选项)
  user: {
    handler(newUser) {
      // 监听user对象的任何属性变化
    },
    deep: true
  }
}

5. 特殊场景与高级用法

5.1 计算属性的setter

computed: {
  fullName: {
    // getter:计算逻辑
    get() {
      return this.firstName + ' ' + this.lastName
    },
    // setter:反向更新
    set(newValue) {
      const names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// 底层实现支持setter
function defineComputed(target, key, userDef) {
  const getter = typeof userDef === 'function' ? userDef : userDef.get
  const setter = userDef.set || noop  // 支持自定义setter
  Object.defineProperty(target, key, {
    get: createComputedGetter(key),
    set: setter
  })
}

5.2 侦听器的立即执行与防抖

watch: {
  searchQuery: {
    handler: 'fetchResults',
    immediate: true,  // 立即执行一次
    deep: false
  }
}
// 结合lodash防抖
import { debounce } from 'lodash'
export default {
  data() {
    return { searchQuery: '' }
  },
  created() {
    // 手动创建防抖的watcher
    this.debouncedFetch = debounce(this.fetchResults, 500)
    this.$watch('searchQuery', this.debouncedFetch)
  },
  methods: {
    fetchResults(query) {
      // API调用
    }
  }
}

5.3 侦听器返回取消函数

export default {
  data() {
    return { pollingInterval: null }
  },
  mounted() {
    // $watch返回取消监听函数
    const unwatch = this.$watch('dataId', (newId) => {
      // 开始轮询
      this.startPolling(newId)
    })
    // 在组件销毁时取消监听
    this.$once('hook:beforeDestroy', () => {
      unwatch()
      this.stopPolling()
    })
  }
}

6. 性能优化与最佳实践

6.1 计算属性的性能优势

6.1.1 缓存避免重复计算
// 低效:每次渲染都重新计算
template: `<div>{{ expensiveComputation() }}</div>`
methods: {
  expensiveComputation() {
    // 每次渲染都会执行
    return heavyCalculation(this.data)
  }
}
// 高效:使用计算属性缓存
computed: {
  computedResult() {
    // 只在依赖变化时重新计算
    return heavyCalculation(this.data)
  }
}
6.1.2 避免不必要的响应式
// 不推荐:在data中定义计算值
data() {
  return {
    // 这会使fullName成为响应式数据,但实际应由其他数据计算得出
    fullName: this.firstName + ' ' + this.lastName
  }
}
// 推荐:使用计算属性
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}

6.2 侦听器的性能注意事项

6.2.1 避免深度监听的性能开销
// 谨慎使用:深度监听大型对象
watch: {
  largeObject: {
    handler() {
      // 处理变化
    },
    deep: true  // 遍历整个对象,性能开销大
  }
}
// 优化:监听具体属性或使用计算属性
watch: {
  'largeObject.importantProp': function(newVal) {
    // 只监听关键属性
  }
}
6.2.2 及时清理侦听器
export default {
  data() {
    return { externalData: null }
  },
  created() {
    // 创建多个侦听器
    this.unwatch1 = this.$watch('filter', this.onFilterChange)
    this.unwatch2 = this.$watch('sortBy', this.onSortChange)
  },
  beforeDestroy() {
    // 清理侦听器,避免内存泄漏
    this.unwatch1()
    this.unwatch2()
  }
}

7. Vue 3中的改进

7.1 Composition API中的实现

import { ref, computed, watch, watchEffect } from 'vue'
export default {
  setup() {
    const firstName = ref('John')
    const lastName = ref('Doe')
    // 计算属性(响应式引用)
    const fullName = computed(() => {
      return firstName.value + ' ' + lastName.value
    })
    // 侦听器(支持多个源)
    watch(
      [firstName, lastName],
      ([newFirst, newLast], [oldFirst, oldLast]) => {
        console.log('Name changed:', newFirst, newLast)
      }
    )
    // 自动依赖追踪的watchEffect
    watchEffect(() => {
      // 自动追踪内部使用的响应式数据
      document.title = `${firstName.value} ${lastName.value}`
    })
    return { firstName, lastName, fullName }
  }
}

7.2 Vue 3的底层优化

// Vue 3使用Proxy实现响应式
const reactiveData = reactive({ 
  firstName: 'John',
  lastName: 'Doe'
})
// 计算属性的实现更高效
const fullName = computed(() => {
  return reactiveData.firstName + ' ' + reactiveData.lastName
})
// 侦听器支持更多选项
watch(
  () => reactiveData.firstName,
  (newVal, oldVal) => {
    console.log('First name changed')
  },
  {
    immediate: true,
    flush: 'post'  // 在组件更新后执行
  }
)

8. 总结

8.1 核心区别总结表

特性computedwatch
设计目的声明派生状态执行副作用
缓存有缓存,依赖不变不重新计算无缓存,每次变化都执行
执行时机惰性求值,访问时计算主动执行,变化时触发
返回值必须返回值不需要返回值
异步支持必须是同步计算支持异步操作
依赖声明自动收集依赖需要显式声明监听目标
使用场景模板中的计算、数据格式化异步操作、复杂逻辑、调试

8.2 选择指南

使用computed当:

  • 需要基于其他数据计算新值
  • 需要在模板中使用的派生数据
  • 需要缓存优化性能
  • 计算逻辑是纯函数,无副作用

使用watch当:

  • 需要在数据变化时执行异步操作
  • 需要执行副作用(API调用、DOM操作等)
  • 需要知道数据变化前后的值
  • 需要响应非响应式数据的变化

8.3 最佳实践建议

  1. 优先使用computed:对于派生数据,计算属性通常是更好的选择
  2. 避免在computed中执行副作用:保持计算属性的纯函数特性
  3. 谨慎使用deep watch:深度监听有性能开销,尽量监听具体路径
  4. 及时清理watch:在组件销毁时取消不需要的侦听器
  5. 考虑使用watchEffect:在Vue 3中,对于自动依赖追踪的场景更简洁

理解computed和watch的底层实现原理,可以帮助开发者更好地使用Vue的响应式系统,编写出更高效、更可维护的代码。

到此这篇关于Vue中的computed与watch底层实现原理最佳实践的文章就介绍到这了,更多相关vue computed与watch底层原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Vue中的验证登录状态的实现方法

    Vue中的验证登录状态的实现方法

    这篇文章主要介绍了Vue中的验证登录状态的实现方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-03-03
  • vue中实现图片压缩 file文件的方法

    vue中实现图片压缩 file文件的方法

    这篇文章主要介绍了vue中实现图片压缩 file文件的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05
  • 10分钟快速上手VueRouter4.x教程

    10分钟快速上手VueRouter4.x教程

    Vue Router目前最新版本是4.X,本文主要主要介绍了10分钟快速上手VueRouter4.x教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • vue2.0 解决抽取公用js的问题

    vue2.0 解决抽取公用js的问题

    这篇文章主要介绍了vue2.0 解决抽取公用js的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-07-07
  • vue3+vite自定义封装vue组件发布到npm包的全过程

    vue3+vite自定义封装vue组件发布到npm包的全过程

    当市面上主流的组件库不能满足我们业务需求的时候,那么我们就有必要开发一套属于自己团队的组件库,下面这篇文章主要给大家介绍了关于vue3+vite自定义封装vue组件发布到npm包的相关资料,需要的朋友可以参考下
    2022-09-09
  • vue + vuex todolist的实现示例代码

    vue + vuex todolist的实现示例代码

    这篇文章主要介绍了vue + vuex todolist的实现示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-03-03
  • 一起来看看Vue的核心原理剖析

    一起来看看Vue的核心原理剖析

    这篇文章主要为大家详细介绍了Vue的核心原理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • 在vue中使用jsonp进行跨域请求接口操作

    在vue中使用jsonp进行跨域请求接口操作

    这篇文章主要介绍了在vue中使用jsonp进行跨域请求接口操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • Vue中created和mounted使用详解

    Vue中created和mounted使用详解

    Vue中生命周期包括多个阶段,如created和mounted,每阶段有特定钩子函数,生命周期与浏览器渲染过程密切相关,了解这些可以优化页面渲染和数据处理,created阶段适用于数据初始化,而mounted阶段适合进行DOM操作和页面渲染后的处理
    2024-10-10
  • 在vue中使用el-tab-pane v-show/v-if无效的解决

    在vue中使用el-tab-pane v-show/v-if无效的解决

    这篇文章主要介绍了在vue中使用el-tab-pane v-show/v-if无效的解决,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08

最新评论