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 = false2.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 核心区别总结表
| 特性 | computed | watch |
|---|---|---|
| 设计目的 | 声明派生状态 | 执行副作用 |
| 缓存 | 有缓存,依赖不变不重新计算 | 无缓存,每次变化都执行 |
| 执行时机 | 惰性求值,访问时计算 | 主动执行,变化时触发 |
| 返回值 | 必须返回值 | 不需要返回值 |
| 异步支持 | 必须是同步计算 | 支持异步操作 |
| 依赖声明 | 自动收集依赖 | 需要显式声明监听目标 |
| 使用场景 | 模板中的计算、数据格式化 | 异步操作、复杂逻辑、调试 |
8.2 选择指南
使用computed当:
- 需要基于其他数据计算新值
- 需要在模板中使用的派生数据
- 需要缓存优化性能
- 计算逻辑是纯函数,无副作用
使用watch当:
- 需要在数据变化时执行异步操作
- 需要执行副作用(API调用、DOM操作等)
- 需要知道数据变化前后的值
- 需要响应非响应式数据的变化
8.3 最佳实践建议
- 优先使用computed:对于派生数据,计算属性通常是更好的选择
- 避免在computed中执行副作用:保持计算属性的纯函数特性
- 谨慎使用deep watch:深度监听有性能开销,尽量监听具体路径
- 及时清理watch:在组件销毁时取消不需要的侦听器
- 考虑使用watchEffect:在Vue 3中,对于自动依赖追踪的场景更简洁
理解computed和watch的底层实现原理,可以帮助开发者更好地使用Vue的响应式系统,编写出更高效、更可维护的代码。
到此这篇关于Vue中的computed与watch底层实现原理最佳实践的文章就介绍到这了,更多相关vue computed与watch底层原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
vue3+vite自定义封装vue组件发布到npm包的全过程
当市面上主流的组件库不能满足我们业务需求的时候,那么我们就有必要开发一套属于自己团队的组件库,下面这篇文章主要给大家介绍了关于vue3+vite自定义封装vue组件发布到npm包的相关资料,需要的朋友可以参考下2022-09-09
在vue中使用el-tab-pane v-show/v-if无效的解决
这篇文章主要介绍了在vue中使用el-tab-pane v-show/v-if无效的解决,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧2020-08-08


最新评论