🎯 学习目标:深入理解Vue3响应式系统中computed、watch、watchEffect的原理机制和实际应用场景
📊 难度等级 :中级-高级
🏷️ 技术标签 :
#Vue3
#响应式系统
#computed
#watch
#watchEffect
⏱️ 阅读时间:约15分钟
🌟 引言
在Vue3的响应式系统中,你是否遇到过这样的困扰:
- 性能问题:不知道什么时候用computed,什么时候用watch,导致不必要的重复计算
- 监听困惑:watch和watchEffect看起来很相似,但不清楚具体区别和使用场景
- 原理不明:只会用API但不理解底层原理,遇到复杂场景就束手无策
- 最佳实践:缺乏实际项目中的最佳实践指导,代码质量参差不齐
今天分享Vue3响应式系统的3个核心API的深度解析,让你的Vue开发更加得心应手!
💡 核心技巧详解
1. computed:智能缓存的计算属性
🔍 应用场景
当你需要基于响应式数据进行复杂计算,且希望结果能够缓存以避免重复计算时使用。
❌ 常见问题
很多开发者习惯在模板中直接进行计算或使用methods
vue
<template>
<!-- ❌ 模板中直接计算,每次渲染都会执行 -->
<div>{{ user.firstName + ' ' + user.lastName }}</div>
<div>{{ getFullName() }}</div>
<!-- ❌ 使用methods,每次访问都会重新计算 -->
<div>{{ calculateTotal() }}</div>
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({
firstName: '张',
lastName: '三'
})
// ❌ methods方式,没有缓存
const getFullName = () => {
console.log('计算全名') // 每次都会执行
return user.firstName + ' ' + user.lastName
}
const calculateTotal = () => {
console.log('计算总价') // 每次都会执行
return items.reduce((sum, item) => sum + item.price, 0)
}
</script>
✅ 推荐方案
使用computed实现智能缓存
vue
<template>
<!-- ✅ 使用computed,有缓存机制 -->
<div>{{ fullName }}</div>
<div>{{ totalPrice }}</div>
</template>
<script setup>
import { reactive, computed } from 'vue'
const user = reactive({
firstName: '张',
lastName: '三'
})
const items = reactive([
{ name: '商品1', price: 100 },
{ name: '商品2', price: 200 }
])
/**
* 计算用户全名
* @description 基于firstName和lastName计算完整姓名,具有缓存机制
* @returns {string} 完整的用户姓名
*/
const fullName = computed(() => {
console.log('计算全名') // 只有依赖变化时才执行
return `${user.firstName} ${user.lastName}`
})
/**
* 计算商品总价
* @description 计算购物车中所有商品的总价格
* @returns {number} 商品总价
*/
const totalPrice = computed(() => {
console.log('计算总价') // 只有items变化时才执行
return items.reduce((sum, item) => sum + item.price, 0)
})
</script>
🔍 computed底层实现原理
computed的核心特性是懒计算 和智能缓存,让我们深入了解其底层实现机制:
1. computed的完整实现结构
javascript
/**
* computed的完整底层实现
* @param {Function} getterOrOptions - 计算函数或配置对象
* @param {Object} debugOptions - 调试选项
*/
function computed(getterOrOptions, debugOptions) {
let getter, setter
// 处理参数:支持函数或对象形式
if (typeof getterOrOptions === 'function') {
getter = getterOrOptions
setter = () => {
console.warn('computed value is readonly')
}
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 🔑 关键:创建computed实例
const cRef = new ComputedRefImpl(getter, setter, debugOptions)
return cRef
}
/**
* ComputedRefImpl - computed的核心实现类
*/
class ComputedRefImpl {
constructor(getter, setter, isReadonly = false) {
this._getter = getter
this._setter = setter
this._value = undefined
this._dirty = true // 🔑 脏检查标记:控制是否需要重新计算
this.effect = null // 存储关联的effect
this.__v_isRef = true
this.__v_isReadonly = isReadonly
// 🔑 关键:创建响应式effect,但设置为懒执行
this.effect = new ReactiveEffect(getter, () => {
// 🔑 调度器:依赖变化时的处理逻辑
if (!this._dirty) {
this._dirty = true
// 触发computed本身的依赖更新
triggerRefValue(this)
}
})
this.effect.computed = this
}
// 🔑 关键:getter访问器 - 实现懒计算和缓存
get value() {
// 收集computed作为依赖
trackRefValue(this)
// 🔑 核心逻辑:只有在脏数据时才重新计算
if (this._dirty) {
this._dirty = false
// 执行计算函数,收集内部依赖
this._value = this.effect.run()
}
return this._value
}
// setter访问器
set value(newValue) {
this._setter(newValue)
}
}
2. 懒计算机制的实现细节
javascript
/**
* ReactiveEffect - computed使用的effect实现
*/
class ReactiveEffect {
constructor(fn, scheduler = null) {
this.fn = fn
this.scheduler = scheduler
this.active = true
this.deps = [] // 存储依赖的集合
}
// 🔑 关键:run方法 - 执行计算函数并收集依赖
run() {
if (!this.active) {
return this.fn()
}
try {
// 设置当前活跃的effect
activeEffect = this
// 🔑 关键:清理旧依赖,重新收集
cleanupEffect(this)
// 执行计算函数,期间会触发依赖收集
return this.fn()
} finally {
// 重置活跃effect
activeEffect = null
}
}
// 停止effect
stop() {
if (this.active) {
cleanupEffect(this)
this.active = false
}
}
}
/**
* 清理effect的依赖关系
*/
function cleanupEffect(effect) {
effect.deps.forEach(dep => {
dep.delete(effect)
})
effect.deps.length = 0
}
3. 缓存机制的核心逻辑
javascript
/**
* 依赖收集:trackRefValue
* @description 当访问computed.value时调用
*/
function trackRefValue(ref) {
if (activeEffect) {
// 🔑 关键:将computed作为依赖源进行追踪
trackEffects(ref.dep || (ref.dep = new Set()))
}
}
/**
* 依赖触发:triggerRefValue
* @description 当computed的依赖发生变化时调用
*/
function triggerRefValue(ref) {
if (ref.dep) {
// 🔑 关键:触发所有依赖computed的effect
triggerEffects(ref.dep)
}
}
/**
* 触发effects执行
*/
function triggerEffects(dep) {
for (const effect of dep) {
if (effect !== activeEffect) {
if (effect.scheduler) {
// 🔑 关键:使用调度器控制执行时机
effect.scheduler()
} else {
effect.run()
}
}
}
}
4. 完整的工作流程示例
javascript
/**
* 演示computed的完整工作流程
*/
const state = reactive({ count: 1, multiplier: 2 })
// 1. 创建computed
const doubleCount = computed(() => {
console.log('🔄 计算函数执行')
return state.count * state.multiplier
})
console.log('📝 computed创建完成,但计算函数尚未执行')
// 2. 第一次访问 - 触发计算和依赖收集
console.log('🔍 第一次访问:', doubleCount.value)
// 输出:🔄 计算函数执行
// 输出:🔍 第一次访问: 2
// 3. 第二次访问 - 使用缓存
console.log('🔍 第二次访问:', doubleCount.value)
// 输出:🔍 第二次访问: 2 (没有"计算函数执行",说明使用了缓存)
// 4. 修改依赖 - 标记为脏数据但不立即计算
console.log('📝 修改state.count = 3')
state.count = 3
console.log('📝 依赖已变化,computed被标记为dirty,但尚未重新计算')
// 5. 再次访问 - 重新计算
console.log('🔍 第三次访问:', doubleCount.value)
// 输出:🔄 计算函数执行
// 输出:🔍 第三次访问: 6
5. 性能优化的关键点
javascript
/**
* computed性能优化的核心机制
*/
// ✅ 优化点1:懒执行 - 只有被访问时才计算
const expensiveComputed = computed(() => {
console.log('执行昂贵的计算...')
// 这里的计算只有在被访问时才会执行
return heavyCalculation()
})
// ✅ 优化点2:智能缓存 - 依赖不变时复用结果
const cachedResult = computed(() => {
return state.list.filter(item => item.active).length
})
// 多次访问使用缓存
console.log(cachedResult.value) // 执行计算
console.log(cachedResult.value) // 使用缓存
console.log(cachedResult.value) // 使用缓存
// ✅ 优化点3:精确依赖追踪 - 只有真正的依赖变化才重新计算
const smartComputed = computed(() => {
// 只依赖state.a,state.b的变化不会触发重新计算
return state.a * 2
})
state.b = 100 // 不会触发smartComputed重新计算
state.a = 5 // 会触发smartComputed重新计算
🧠 computed工作机制总结
- 创建阶段:创建ComputedRefImpl实例,设置懒执行的ReactiveEffect
- 首次访问:执行计算函数,收集依赖,缓存结果,标记为clean
- 缓存访问:直接返回缓存值,不执行计算函数
- 依赖变化:调度器将computed标记为dirty,触发computed的依赖更新
- 重新计算:下次访问时检测到dirty,重新执行计算函数并更新缓存
💡 核心要点
- 缓存机制:只有依赖的响应式数据发生变化时才重新计算
- 惰性求值:只有被访问时才会执行计算函数
- 依赖追踪:自动追踪计算函数中使用的响应式数据
🎯 实际应用
在电商项目中计算购物车信息
vue
<script setup>
import { reactive, computed } from 'vue'
const cart = reactive({
items: [
{ id: 1, name: 'iPhone', price: 6999, quantity: 1, selected: true },
{ id: 2, name: 'iPad', price: 3999, quantity: 2, selected: false },
{ id: 3, name: 'MacBook', price: 12999, quantity: 1, selected: true }
]
})
/**
* 计算已选中的商品
* @description 过滤出用户选中的商品列表
* @returns {Array} 已选中的商品数组
*/
const selectedItems = computed(() => {
return cart.items.filter(item => item.selected)
})
/**
* 计算选中商品的总价
* @description 计算用户选中商品的总金额
* @returns {number} 选中商品总价
*/
const selectedTotal = computed(() => {
return selectedItems.value.reduce((sum, item) => {
return sum + (item.price * item.quantity)
}, 0)
})
/**
* 计算优惠后价格
* @description 根据总价计算优惠后的最终价格
* @returns {number} 优惠后价格
*/
const finalPrice = computed(() => {
const total = selectedTotal.value
if (total > 10000) return total * 0.9 // 9折
if (total > 5000) return total * 0.95 // 95折
return total
})
</script>
2. watch:精确监听的数据观察者
🔍 应用场景
当你需要在数据变化时执行副作用操作(如API调用、DOM操作等)时使用。
❌ 常见问题
不理解watch的监听机制和配置选项
vue
<script setup>
import { ref, reactive } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三', age: 25 })
// ❌ 直接监听reactive对象,无法获取旧值
watch(user, (newVal, oldVal) => {
console.log(newVal, oldVal) // oldVal和newVal是同一个对象
})
// ❌ 监听深层属性但没有开启deep选项
watch(user.name, (newVal) => {
console.log('用户名变化:', newVal) // 不会触发
})
</script>
✅ 推荐方案
正确使用watch的各种监听方式
vue
<script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const user = reactive({
name: '张三',
age: 25,
profile: {
email: 'zhangsan@example.com'
}
})
/**
* 监听ref值的变化
* @description 监听count的变化并执行相应操作
*/
watch(count, (newVal, oldVal) => {
console.log(`count从 ${oldVal} 变为 ${newVal}`)
// 执行副作用操作
updateCountInAPI(newVal)
})
/**
* 监听reactive对象的特定属性
* @description 使用getter函数监听user.name的变化
*/
watch(
() => user.name,
(newName, oldName) => {
console.log(`用户名从 ${oldName} 变为 ${newName}`)
// 更新用户信息到服务器
updateUserName(newName)
}
)
/**
* 深度监听reactive对象
* @description 监听user对象的所有属性变化
*/
watch(
user,
(newUser, oldUser) => {
console.log('用户信息发生变化')
// 保存用户信息
saveUserProfile(newUser)
},
{ deep: true }
)
/**
* 监听多个数据源
* @description 同时监听多个响应式数据的变化
*/
watch(
[count, () => user.name],
([newCount, newName], [oldCount, oldName]) => {
console.log('count或用户名发生变化')
// 执行复合逻辑
handleMultipleChanges(newCount, newName)
}
)
/**
* 立即执行的监听器
* @description 组件挂载时立即执行一次
*/
watch(
() => user.profile.email,
(newEmail) => {
console.log('邮箱变化:', newEmail)
validateEmail(newEmail)
},
{ immediate: true }
)
// 模拟API调用函数
const updateCountInAPI = (count) => {
console.log(`API调用: 更新count为 ${count}`)
}
const updateUserName = (name) => {
console.log(`API调用: 更新用户名为 ${name}`)
}
const saveUserProfile = (user) => {
console.log('API调用: 保存用户信息', user)
}
const handleMultipleChanges = (count, name) => {
console.log(`复合操作: count=${count}, name=${name}`)
}
const validateEmail = (email) => {
console.log(`验证邮箱: ${email}`)
}
</script>
🔍 watch底层实现原理
watch的核心特性是精确监听 和回调执行,让我们深入了解其底层实现机制:
1. watch的完整实现结构
javascript
/**
* watch的完整底层实现
* @param {Function|Object|Array} source - 监听源
* @param {Function} cb - 回调函数
* @param {Object} options - 配置选项
*/
function watch(source, cb, options = {}) {
return doWatch(source, cb, options)
}
/**
* doWatch - watch的核心实现函数
*/
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = {}) {
let getter
let forceTrigger = false
let isMultiSource = false
// 🔑 关键:处理不同类型的监听源
if (isRef(source)) {
// 监听ref
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// 监听reactive对象
getter = () => source
deep = true // reactive对象默认深度监听
} else if (Array.isArray(source)) {
// 监听多个源
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () => source.map(s => {
if (isRef(s)) return s.value
if (isReactive(s)) return traverse(s)
if (typeof s === 'function') return callWithErrorHandling(s)
return s
})
} else if (typeof source === 'function') {
// 监听函数返回值
if (cb) {
// watch(fn, callback)
getter = () => callWithErrorHandling(source)
} else {
// watchEffect(fn)
getter = () => {
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(source, [onCleanup])
}
}
} else {
getter = () => {}
}
// 🔑 关键:深度监听的实现
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup
let onCleanup = (fn) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn)
}
}
let oldValue = isMultiSource ? [] : {}
// 🔑 关键:调度器 - 控制回调执行时机
const job = () => {
if (!effect.active) return
if (cb) {
// watch with callback
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? newValue.some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue))
) {
// 清理副作用
if (cleanup) {
cleanup()
}
// 🔑 关键:执行回调函数
callWithAsyncErrorHandling(cb, [
newValue,
oldValue === {} ? undefined : oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
// 设置调度器的执行时机
let scheduler
if (flush === 'sync') {
scheduler = job // 同步执行
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job) // 组件更新后执行
} else {
// 默认 'pre' - 组件更新前执行
scheduler = () => queuePreFlushCb(job)
}
// 🔑 关键:创建响应式effect
const effect = new ReactiveEffect(getter, scheduler)
// 初始执行
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(effect.run.bind(effect))
} else {
effect.run()
}
// 返回停止监听的函数
return () => {
effect.stop()
}
}
2. 深度遍历机制的实现
javascript
/**
* traverse - 深度遍历函数,用于深度监听
* @param {*} value - 要遍历的值
* @param {Set} seen - 已访问的对象集合,防止循环引用
*/
function traverse(value, seen) {
if (!isObject(value) || value[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
// 🔑 关键:防止循环引用
if (seen.has(value)) {
return value
}
seen.add(value)
// 🔑 关键:根据不同类型进行遍历
if (isRef(value)) {
// 遍历ref的value
traverse(value.value, seen)
} else if (Array.isArray(value)) {
// 遍历数组的每个元素
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen)
}
} else if (isSet(value) || isMap(value)) {
// 遍历Set和Map
value.forEach((v) => {
traverse(v, seen)
})
} else if (isPlainObject(value)) {
// 🔑 关键:遍历对象的所有属性,触发getter收集依赖
for (const key in value) {
traverse(value[key], seen)
}
}
return value
}
3. 监听源处理的具体实现
javascript
/**
* 不同监听源的处理策略
*/
// 1. 监听ref
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`)
})
// 实际监听的是 () => count.value
// 2. 监听reactive对象的特定属性
const state = reactive({ user: { name: 'Vue' } })
watch(() => state.user.name, (newVal, oldVal) => {
console.log(`name: ${oldVal} -> ${newVal}`)
})
// getter函数会在执行时收集state.user.name的依赖
// 3. 监听整个reactive对象(深度监听)
watch(state, (newVal, oldVal) => {
console.log('state changed')
}, { deep: true })
// 会调用traverse(state)遍历所有属性
// 4. 监听多个源
watch([count, () => state.user.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}`)
console.log(`name: ${oldName} -> ${newName}`)
})
4. 调度器和执行时机控制
javascript
/**
* watch的调度器实现 - 控制回调执行时机
*/
// 🔑 关键:不同的flush选项控制执行时机
const flushJobs = {
// 同步执行 - 立即执行
sync: (job) => job(),
// 组件更新前执行 - 默认行为
pre: (job) => {
queuePreFlushCb(job)
},
// 组件更新后执行
post: (job) => {
queuePostRenderEffect(job)
}
}
/**
* 队列管理 - 批量处理回调
*/
const queue = []
let isFlushing = false
let isFlushPending = false
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
}
queueFlush()
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
nextTick(flushJobs)
}
}
function flushJobs() {
isFlushPending = false
isFlushing = true
// 🔑 关键:按顺序执行所有job
queue.sort((a, b) => getId(a) - getId(b))
for (let i = 0; i < queue.length; i++) {
const job = queue[i]
if (job && job.active !== false) {
callWithErrorHandling(job)
}
}
queue.length = 0
isFlushing = false
}
5. 完整的工作流程示例
javascript
/**
* 演示watch的完整工作流程
*/
const state = reactive({
count: 0,
user: {
name: 'Vue',
profile: {
age: 3
}
}
})
// 1. 创建watch - 设置监听和回调
const stopWatcher = watch(
() => state.user.name, // getter函数
(newName, oldName) => {
console.log(`🔄 用户名从 ${oldName} 变为 ${newName}`)
},
{ immediate: true } // 立即执行一次
)
console.log('📝 watch创建完成')
// 输出:🔄 用户名从 undefined 变为 Vue (immediate执行)
// 2. 修改监听的属性 - 触发回调
console.log('📝 修改用户名')
state.user.name = 'React'
// 输出:🔄 用户名从 Vue 变为 React
// 3. 修改非监听的属性 - 不触发回调
console.log('📝 修改年龄(不会触发回调)')
state.user.profile.age = 4
// 无输出,因为没有监听age属性
// 4. 停止监听
console.log('📝 停止监听')
stopWatcher()
// 5. 再次修改 - 不会触发回调
state.user.name = 'Angular'
// 无输出,因为已停止监听
6. 性能优化策略
javascript
/**
* watch性能优化的关键技巧
*/
// ✅ 优化点1:精确监听 - 只监听需要的属性
watch(() => state.user.name, callback) // 只监听name
// 而不是
watch(state.user, callback, { deep: true }) // 监听整个user对象
// ✅ 优化点2:使用computed优化复杂计算
const expensiveValue = computed(() => {
return state.list.filter(item => item.active).length
})
watch(expensiveValue, callback) // 监听computed而不是原始数据
// ✅ 优化点3:合理使用flush选项
watch(source, callback, { flush: 'post' }) // 在DOM更新后执行
// ✅ 优化点4:及时清理监听器
const stop = watch(source, callback)
onUnmounted(() => {
stop() // 组件卸载时停止监听
})
🧠 watch工作机制总结
- 创建阶段:解析监听源,创建getter函数和调度器
- 依赖收集:执行getter函数,收集相关依赖
- 监听等待:等待依赖变化,不执行任何操作
- 变化检测:依赖变化时,调度器重新执行getter获取新值
- 回调执行:比较新旧值,如有变化则执行回调函数
- 清理机制:支持手动停止监听和自动清理
💡 核心要点
- 精确监听:可以监听特定的数据源,不会产生不必要的触发
- 获取新旧值:能够获取变化前后的值,便于对比处理
- 配置选项:支持deep、immediate等配置选项
🎯 实际应用
在搜索功能中实现防抖和API调用
vue
<script setup>
import { ref, watch } from 'vue'
const searchKeyword = ref('')
const searchResults = ref([])
const loading = ref(false)
/**
* 搜索API调用函数
* @description 调用搜索接口获取结果
* @param {string} keyword - 搜索关键词
* @returns {Promise<Array>} 搜索结果
*/
const searchAPI = async (keyword) => {
const response = await fetch(`/api/search?q=${keyword}`)
return response.json()
}
/**
* 防抖搜索监听器
* @description 监听搜索关键词变化,实现防抖搜索
*/
let searchTimer = null
watch(
searchKeyword,
async (newKeyword, oldKeyword) => {
// 清除之前的定时器
if (searchTimer) {
clearTimeout(searchTimer)
}
// 如果关键词为空,清空结果
if (!newKeyword.trim()) {
searchResults.value = []
return
}
// 设置防抖延迟
searchTimer = setTimeout(async () => {
try {
loading.value = true
console.log(`搜索关键词从 "${oldKeyword}" 变为 "${newKeyword}"`)
const results = await searchAPI(newKeyword)
searchResults.value = results
} catch (error) {
console.error('搜索失败:', error)
searchResults.value = []
} finally {
loading.value = false
}
}, 300) // 300ms防抖延迟
}
)
</script>
3. watchEffect:自动依赖追踪的副作用函数
🔍 应用场景
当你需要执行副作用操作,且希望自动追踪依赖而不需要明确指定监听目标时使用。
❌ 常见问题
不理解watchEffect的自动依赖追踪机制
vue
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('张三')
// ❌ 在watchEffect中使用了响应式数据但没有正确理解依赖追踪
watchEffect(() => {
console.log('副作用执行')
// 只使用了count,但开发者可能认为name变化也会触发
if (count.value > 0) {
console.log(`count: ${count.value}`)
}
// name没有被使用,所以name变化不会触发这个watchEffect
})
</script>
✅ 推荐方案
正确使用watchEffect的自动依赖追踪
vue
<script setup>
import { ref, reactive, watchEffect } from 'vue'
const count = ref(0)
const user = reactive({
name: '张三',
age: 25
})
const isVisible = ref(true)
/**
* 自动依赖追踪的副作用函数
* @description 自动追踪函数中使用的所有响应式数据
*/
watchEffect(() => {
console.log('watchEffect执行')
// 这里使用了count和user.name,所以这两个数据变化都会触发
if (count.value > 0) {
console.log(`用户 ${user.name} 的计数: ${count.value}`)
}
// 条件性依赖:只有当isVisible为true时,才会追踪user.age
if (isVisible.value) {
console.log(`用户年龄: ${user.age}`)
}
})
/**
* 清理副作用的watchEffect
* @description 演示如何在watchEffect中进行清理操作
*/
watchEffect((onInvalidate) => {
const timer = setInterval(() => {
console.log(`定时器执行,当前count: ${count.value}`)
}, 1000)
// 注册清理函数
onInvalidate(() => {
console.log('清理定时器')
clearInterval(timer)
})
})
/**
* 停止监听的示例
* @description 演示如何手动停止watchEffect
*/
const stopWatcher = watchEffect(() => {
console.log(`监听用户名变化: ${user.name}`)
})
// 在某个条件下停止监听
const handleStopWatching = () => {
stopWatcher()
console.log('已停止监听')
}
</script>
🔍 watchEffect底层实现原理
watchEffect的核心特性是自动依赖收集 和立即执行,让我们深入了解其底层实现机制:
1. watchEffect的完整实现结构
javascript
/**
* watchEffect的完整底层实现
* @param {Function} effect - 副作用函数
* @param {Object} options - 配置选项
*/
function watchEffect(effect, options) {
return doWatch(effect, null, options)
}
/**
* watchPostEffect - 在组件更新后执行的watchEffect
*/
function watchPostEffect(effect, options) {
return doWatch(
effect,
null,
{ ...options, flush: 'post' }
)
}
/**
* watchSyncEffect - 同步执行的watchEffect
*/
function watchSyncEffect(effect, options) {
return doWatch(
effect,
null,
{ ...options, flush: 'sync' }
)
}
/**
* doWatch中watchEffect的特殊处理逻辑
*/
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = {}) {
let getter
let cleanup
// 🔑 关键:watchEffect的特殊处理(cb为null)
if (!cb) {
// watchEffect模式
let onCleanup = (fn) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn)
}
}
// 🔑 关键:包装effect函数,注入清理机制
getter = () => {
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(source, [onCleanup])
}
}
// 🔑 关键:调度器 - 控制执行时机
const job = () => {
if (!effect.active) return
if (!cb) {
// watchEffect模式 - 直接执行
effect.run()
}
}
// 设置调度器
let scheduler
if (flush === 'sync') {
scheduler = job // 同步执行
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job) // 组件更新后执行
} else {
// 默认 'pre' - 组件更新前执行
scheduler = () => queuePreFlushCb(job)
}
// 🔑 关键:创建响应式effect
const effect = new ReactiveEffect(getter, scheduler)
// 🔑 关键:立即执行(watchEffect的核心特性)
if (flush === 'post') {
queuePostRenderEffect(effect.run.bind(effect))
} else {
effect.run() // 立即执行,自动收集依赖
}
// 返回停止函数
return () => {
effect.stop()
}
}
2. 自动依赖收集机制的实现
javascript
/**
* ReactiveEffect - 响应式effect的核心实现
*/
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
this.fn = fn
this.scheduler = scheduler
this.active = true
this.deps = [] // 🔑 关键:存储依赖关系
this.parent = undefined
recordEffectScope(this, scope)
}
// 🔑 关键:执行effect并收集依赖
run() {
if (!this.active) {
return this.fn()
}
let parent = activeEffect
let lastShouldTrack = shouldTrack
try {
// 🔑 关键:设置当前活跃的effect
this.parent = activeEffect
activeEffect = this
shouldTrack = true
// 🔑 关键:清理旧的依赖关系
cleanupEffect(this)
// 🔑 关键:执行函数,期间访问的响应式数据会自动收集依赖
return this.fn()
} finally {
// 恢复之前的状态
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
}
}
// 停止effect
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
/**
* 清理effect的依赖关系
*/
function cleanupEffect(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect) // 从依赖集合中移除当前effect
}
deps.length = 0
}
}
3. 依赖收集和触发机制
javascript
/**
* track - 依赖收集函数
* 在响应式对象的getter中调用
*/
function track(target, type, key) {
if (shouldTrack && activeEffect) {
// 🔑 关键:获取目标对象的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 🔑 关键:获取特定属性的依赖集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 🔑 关键:建立双向依赖关系
trackEffects(dep)
}
}
/**
* trackEffects - 建立effect和依赖的双向关系
*/
function trackEffects(dep) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // 标记为新依赖
shouldTrack = !wasTracked(dep)
}
} else {
// 降级处理
shouldTrack = !dep.has(activeEffect)
}
if (shouldTrack) {
// 🔑 关键:双向绑定
dep.add(activeEffect) // 依赖集合中添加当前effect
activeEffect.deps.push(dep) // effect中记录依赖
}
}
/**
* trigger - 依赖触发函数
* 在响应式对象的setter中调用
*/
function trigger(target, type, key, newValue, oldValue, oldTarget) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有依赖,直接返回
return
}
let deps = []
// 🔑 关键:收集需要触发的effect
if (type === TriggerOpTypes.CLEAR) {
// 清空操作,触发所有依赖
deps = [...depsMap.values()]
} else if (key === 'length' && Array.isArray(target)) {
// 数组长度变化的特殊处理
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newValue) {
deps.push(dep)
}
})
} else {
// 普通属性变化
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// 处理添加/删除操作
switch (type) {
case TriggerOpTypes.ADD:
if (!Array.isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
} else if (isIntegerKey(key)) {
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!Array.isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
// 🔑 关键:触发所有相关的effect
if (deps.length === 1) {
if (deps[0]) {
triggerEffects(deps[0])
}
} else {
const effects = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
triggerEffects(createDep(effects))
}
}
4. 清理机制的完整实现
javascript
/**
* watchEffect的清理机制实现
*/
// 示例:完整的清理机制演示
const state = reactive({ count: 0 })
const stop = watchEffect((onCleanup) => {
console.log('🔄 watchEffect执行,count:', state.count)
// 🔑 关键:创建副作用(定时器、事件监听器、网络请求等)
const timer = setInterval(() => {
console.log('⏰ 定时器tick')
}, 1000)
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => {
console.log('📡 数据获取成功:', data)
})
.catch(error => {
if (error.name !== 'AbortError') {
console.error('❌ 请求失败:', error)
}
})
// 🔑 关键:注册清理函数
onCleanup(() => {
console.log('🧹 执行清理操作')
clearInterval(timer) // 清理定时器
controller.abort() // 取消网络请求
})
})
// 触发重新执行 - 会先执行清理函数
setTimeout(() => {
state.count++ // 触发watchEffect重新执行
}, 2000)
// 停止监听 - 会执行最后一次清理
setTimeout(() => {
stop() // 停止watchEffect
}, 5000)
5. 动态依赖收集的实现原理
javascript
/**
* 演示watchEffect的动态依赖收集机制
*/
const state = reactive({
mode: 'light',
theme: {
light: { bg: 'white', text: 'black' },
dark: { bg: 'black', text: 'white' }
},
user: {
name: 'Vue',
avatar: 'vue.png'
},
showUser: true
})
// 🔑 关键:依赖会根据执行路径动态变化
const stop = watchEffect(() => {
console.log('🎨 当前模式:', state.mode)
// 🔑 关键:条件依赖收集
if (state.mode === 'light') {
console.log('☀️ 浅色主题:', state.theme.light.bg)
// 只有在light模式下才会收集theme.light的依赖
} else {
console.log('🌙 深色主题:', state.theme.dark.bg)
// 只有在dark模式下才会收集theme.dark的依赖
}
// 🔑 关键:动态依赖
if (state.showUser) {
console.log('👤 用户信息:', state.user.name)
// 只有当showUser为true时才收集user.name的依赖
}
})
// 测试动态依赖收集
console.log('📝 修改浅色主题背景色')
state.theme.light.bg = 'gray' // 触发(当前是light模式)
console.log('📝 修改深色主题背景色')
state.theme.dark.bg = 'navy' // 不触发(当前不是dark模式)
console.log('📝 切换到深色模式')
state.mode = 'dark' // 触发,且之后深色主题的变化会触发
console.log('📝 再次修改深色主题背景色')
state.theme.dark.bg = 'purple' // 现在会触发(已切换到dark模式)
console.log('📝 隐藏用户信息')
state.showUser = false // 触发,且之后用户信息变化不会触发
console.log('📝 修改用户名')
state.user.name = 'React' // 不触发(showUser为false)
6. 执行时机控制的实现
javascript
/**
* watchEffect的执行时机控制机制
*/
// 🔑 关键:不同flush选项的实现
const state = reactive({ count: 0 })
// 1. 默认 'pre' - 组件更新前执行
watchEffect(() => {
console.log('⚡ pre flush:', state.count)
// 在DOM更新前执行,适合数据预处理
})
// 2. 'post' - 组件更新后执行
watchPostEffect(() => {
console.log('🔄 post flush:', state.count)
// 在DOM更新后执行,适合DOM操作
document.title = `Count: ${state.count}`
})
// 3. 'sync' - 同步执行
watchSyncEffect(() => {
console.log('🚀 sync flush:', state.count)
// 同步执行,谨慎使用,可能影响性能
})
/**
* 队列调度的实现原理
*/
const queue = []
let isFlushing = false
let isFlushPending = false
// pre flush队列
const pendingPreFlushCbs = []
let activePreFlushCbs = null
let preFlushIndex = 0
// post render队列
const pendingPostFlushCbs = []
let activePostFlushCbs = null
let postFlushIndex = 0
function queuePreFlushCb(cb) {
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}
function queuePostFlushCb(cb) {
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}
function queueCb(cb, activeQueue, pendingQueue, index) {
if (!Array.isArray(cb)) {
if (
!activeQueue ||
!activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
) {
pendingQueue.push(cb)
}
} else {
pendingQueue.push(...cb)
}
queueFlush()
}
🧠 watchEffect工作机制总结
- 创建阶段:创建ReactiveEffect实例,设置调度器
- 立即执行:首次运行effect函数,自动收集依赖
- 依赖收集:执行过程中访问的响应式数据自动建立依赖关系
- 监听等待:等待依赖变化,不执行任何操作
- 重新执行:依赖变化时,先执行清理函数,再重新执行effect
- 动态更新:每次执行都会重新收集依赖,实现动态依赖追踪
💡 核心要点
- 自动依赖追踪:自动追踪函数中使用的所有响应式数据
- 立即执行:创建时立即执行一次
- 清理机制:支持清理副作用,避免内存泄漏
🎯 实际应用
在数据同步和日志记录中的应用
vue
<script setup>
import { ref, reactive, watchEffect } from 'vue'
const userSettings = reactive({
theme: 'light',
language: 'zh-CN',
notifications: true
})
const currentPage = ref('home')
const userActivity = ref([])
/**
* 自动保存用户设置
* @description 当用户设置发生变化时自动保存到localStorage
*/
watchEffect(() => {
const settings = {
theme: userSettings.theme,
language: userSettings.language,
notifications: userSettings.notifications
}
console.log('保存用户设置:', settings)
localStorage.setItem('userSettings', JSON.stringify(settings))
})
/**
* 页面访问日志记录
* @description 记录用户的页面访问行为
*/
watchEffect(() => {
const logEntry = {
page: currentPage.value,
timestamp: new Date().toISOString(),
theme: userSettings.theme
}
console.log('记录页面访问:', logEntry)
// 添加到活动记录
userActivity.value.push(logEntry)
// 发送到分析服务
sendAnalytics(logEntry)
})
/**
* WebSocket连接管理
* @description 根据用户设置和页面状态管理WebSocket连接
*/
watchEffect((onInvalidate) => {
// 只有在特定页面且开启通知时才建立连接
if (currentPage.value === 'dashboard' && userSettings.notifications) {
console.log('建立WebSocket连接')
const ws = new WebSocket('ws://localhost:8080')
ws.onopen = () => {
console.log('WebSocket连接已建立')
}
ws.onmessage = (event) => {
console.log('收到消息:', event.data)
}
ws.onerror = (error) => {
console.error('WebSocket错误:', error)
}
// 清理函数:关闭WebSocket连接
onInvalidate(() => {
console.log('关闭WebSocket连接')
ws.close()
})
}
})
// 模拟分析服务调用
const sendAnalytics = (data) => {
console.log('发送分析数据:', data)
// 实际项目中这里会调用分析服务API
}
</script>
📊 技巧对比总结
API | 使用场景 | 优势 | 注意事项 |
---|---|---|---|
computed | 基于响应式数据的计算 | 缓存机制、惰性求值 | 不能有副作用操作 |
watch | 数据变化时的副作用操作 | 精确监听、获取新旧值 | 需要明确指定监听目标 |
watchEffect | 自动依赖追踪的副作用 | 自动依赖追踪、立即执行 | 依赖关系可能不够明确 |
🎯 实战应用建议
最佳实践
- computed应用:用于模板中的计算属性、数据转换、过滤排序等纯计算场景
- watch应用:用于API调用、表单验证、路由监听等需要副作用的场景
- watchEffect应用:用于日志记录、数据同步、自动保存等自动化副作用场景
性能考虑
- computed缓存:充分利用computed的缓存机制,避免重复计算
- watch防抖:对于频繁变化的数据,使用防抖技术优化性能
- watchEffect清理:及时清理副作用,避免内存泄漏和重复执行
选择指南
javascript
// 选择computed的情况
const fullName = computed(() => firstName.value + ' ' + lastName.value)
// 选择watch的情况
watch(searchKeyword, async (newKeyword) => {
const results = await searchAPI(newKeyword)
searchResults.value = results
})
// 选择watchEffect的情况
watchEffect(() => {
localStorage.setItem('settings', JSON.stringify(settings))
})
💡 总结
这3个Vue3响应式API在日常开发中各有所长,掌握它们能让你的Vue开发更加高效:
- computed核心:智能缓存的计算属性,适用于纯计算场景
- watch核心:精确监听的数据观察者,适用于副作用操作
- watchEffect核心:自动依赖追踪的副作用函数,适用于自动化场景
希望这些技巧能帮助你在Vue3开发中更好地处理响应式数据,写出更高效的代码!
🔗 相关资源
💡 今日收获:掌握了Vue3响应式系统的3个核心API,这些知识点在实际开发中非常实用。
如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀