Vue3响应式系统核心对比
effect/track/trigger机制:
- effect负责执行副作用
- track在数据读取时建立依赖
- trigger在数据修改时触发更新
三者协作实现响应式闭环
computed/watch/watchEffect对比:
- computed:有缓存的派生数据计算,适合模板展示
- watch:精确监听特定源,支持异步和旧值访问
- watchEffect:自动追踪依赖,立即执行副作用
性能优化:通过Proxy拦截、依赖清理和调度控制实现高效更新,computed的缓存机制减少重复计算
使用场景:根据是否需要返回值、旧值访问和精确控制依赖选择不同API,组件卸载时自动停止监听
以下是 Vue3 响应式系统中 effect、track、trigger 三个核心函数的对比总结:
effect、track、trigger 对比表
| 维度 | effect | track | trigger |
|---|---|---|---|
| 核心职责 | 收集依赖、执行副作用函数 | 建立响应式数据与effect的依赖关系 | 通知所有依赖该数据的effect重新执行 |
| 触发时机 | 首次执行时手动调用;依赖变化时自动重新执行 | 响应式数据被读取(get)时 | 响应式数据被修改(set)时 |
| 执行角色 | 主动方:创建并管理副作用 | 被动方:被响应式数据的get操作触发 | 主动方:由响应式数据的set操作触发执行 |
| 关键操作 | 1. 将当前effect设为active 2. 执行用户传入的函数 3. 触发函数内的所有get操作 | 1. 获取当前激活的effect 2. 建立target → key → effects的映射关系 3. 将effect存入依赖集合 | 1. 根据target和key查找所有依赖 2. 遍历依赖集合 3. 依次执行每个effect(scheduler或run) |
| 数据结构 | 维护当前激活的effect(全局变量) | 使用WeakMap存储依赖关系: targetMap → target → key → Set(effects) | 从targetMap中查找存储的依赖关系 |
| 使用场景 | computed、watch、render函数、用户自定义副作用 | 响应式对象proxy的get拦截器中 | 响应式对象proxy的set拦截器中 |
| 代码示例 | js<br>effect(() => {<br> console.log(state.count)<br>})<br> |
js<br>function track(target, key) {<br> if (activeEffect) {<br> // 建立依赖关系<br> deps.add(activeEffect)<br> }<br>}<br> |
js<br>function trigger(target, key) {<br> const deps = getDeps(target, key)<br> deps.forEach(effect => effect())<br>}<br> |
| 防止循环 | 通过判断effect !== activeEffect避免无限递归 | 不涉及 | 通过调度器(scheduler)异步执行或去重处理 |
| 嵌套支持 | 使用栈结构维护effect嵌套 | 自动追踪当前激活的最内层effect | 正确触发所有层级的effect |
| 清除机制 | 执行前清除旧依赖,避免遗留依赖 | 支持在effect重新执行前清除过期的依赖关系 | 不涉及 |
| 调度控制 | 支持scheduler选项,可自定义执行时机(如flush: 'sync'/'post'/'pre') | 不涉及 | 根据配置决定同步/异步执行effect |
| 典型应用 | 组件渲染、计算属性、watch监听器 | Vue内部自动调用,开发者通常无需手动调用 | Vue内部自动调用,开发者通常无需手动调用 |
三者协作流程图
1. effect(fn) 执行
↓
2. 设置 activeEffect = currentEffect
↓
3. 执行 fn()
↓
4. fn() 中读取响应式数据 state.count
↓
5. 触发 proxy 的 get 拦截器
↓
6. 调用 track(target, 'count')
↓
7. 建立依赖映射:target → count → Set(effect)
↓
8. fn() 执行完毕,恢复 activeEffect
↓
【等待数据变化】
↓
9. 修改响应式数据 state.count = newValue
↓
10. 触发 proxy 的 set 拦截器
↓
11. 调用 trigger(target, 'count')
↓
12. 从依赖映射中找到所有关联的 effect
↓
13. 依次执行这些 effect(重新执行 fn())
↓
【回到步骤3,形成响应式闭环】
简化版实现示例
javascript
// 核心数据结构
let activeEffect = null
const targetMap = new WeakMap()
// effect:创建响应式副作用
function effect(fn) {
const _effect = function() {
try {
activeEffect = _effect
return fn() // 执行过程会触发track
} finally {
activeEffect = null
}
}
_effect() // 立即执行,收集依赖
return _effect
}
// track:追踪依赖
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect) // 存储当前effect
}
// trigger:触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect()) // 执行所有依赖的effect
}
}
关键要点总结
-
effect是订阅者:负责注册和执行副作用函数
-
track是订阅过程:在数据被读取时,将当前effect订阅到该数据的依赖列表中
-
trigger是发布过程:在数据被修改时,找出所有订阅了该数据的effect并执行
-
三者共同实现了观察者模式:effect是观察者,响应式数据是被观察者,track和trigger是连接两者的桥梁
-
性能优化:通过Proxy拦截、依赖清理、调度器控制等方式,实现了精确的依赖追踪和高效的更新触发
以下是 Vue3 中 computed、watch、watchEffect 三个响应式 API 的详细对比总结:
computed watch watchEffect 对比表
| 维度 | computed | watch | watchEffect |
|---|---|---|---|
| 核心职责 | 计算派生数据,返回响应式 ref 对象 | 显式监听特定数据源,执行副作用 | 自动追踪依赖,执行副作用 |
| 返回值 | 返回一个只读的响应式 ref 对象(需要 .value 访问) |
无返回值(或可返回停止监听的函数) | 无返回值(或可返回停止监听的函数) |
| 依赖追踪 | 自动追踪内部使用的响应式数据 | 显式指定监听的源(数组或单个ref) | 自动追踪函数内部使用的所有响应式数据 |
| 执行时机 | 依赖变化时同步计算,懒执行(仅当被访问时计算) | 依赖变化时执行,可控制 immediate 选项 | 立即执行一次,依赖变化时重新执行 |
| 缓存机制 | ✅ 有缓存:依赖不变时返回缓存值,避免重复计算 | ❌ 无缓存:每次变化都执行回调 | ❌ 无缓存:每次依赖变化都执行 |
| 异步支持 | 不适用(应为同步纯函数) | ✅ 支持在回调中执行异步操作 | ✅ 支持在回调中执行异步操作 |
| 访问旧值 | ❌ 无法获取旧值(只返回新计算值) | ✅ 可获取旧值和新值 | ❌ 无法获取旧值(只有新值) |
| 深度监听 | 自动深度追踪依赖 | 需要设置 deep: true |
自动深度追踪依赖 |
| 立即执行 | 懒执行,仅当被访问时计算 | 可通过 immediate: true 立即执行 |
默认立即执行 |
| 停止监听 | 自动跟随组件卸载 | 手动调用返回的函数可停止 | 手动调用返回的函数可停止 |
| 适用场景 | 依赖其他数据计算新值的场景 模板中需要派生数据 | 执行异步操作 数据变化时执行副作用 需要精确控制监听源 | 不需要旧值的副作用 自动追踪多个依赖 简化 watch 写法 |
| 性能特点 | 优化性能:仅在依赖变化且被访问时重新计算 | 按需监听指定源,更可控 | 自动追踪,但可能过度监听 |
| 代码示例 | js<br>const double = computed(() => <br> state.count * 2<br>)<br> |
js<br>watch(state.count, (newVal, oldVal) => {<br> console.log(newVal, oldVal)<br>})<br> |
js<br>watchEffect(() => {<br> console.log(state.count)<br>})<br> |
| 监听多个源 | 内部自动处理 | 数组形式监听多个源 | 自动追踪函数内所有源 |
| 条件逻辑 | 计算逻辑中可包含条件判断 | 可在回调中编写复杂条件逻辑 | 可在回调中编写复杂条件逻辑 |
| 副作用清理 | 不适用 | 支持 onCleanup 清理副作用 |
支持 onCleanup 清理副作用 |
| flush 时机 | 同步计算,默认 flush: 'pre' |
支持 flush: 'pre'/'post'/'sync' |
支持 flush: 'pre'/'post'/'sync' |
| 类型推断 | TypeScript 自动推断返回类型 | 需要显式标注类型或自动推断 | 自动推断函数内使用的类型 |
| 调试支持 | 支持 onTrack/onTrigger 调试选项 |
支持 onTrack/onTrigger 调试选项 |
支持 onTrack/onTrigger 调试选项 |
flush 是 Vue3 中控制副作用函数执行时机的配置选项,用于决定响应式数据变化后,副作用(watch、watchEffect、组件渲染)在何时执行
使用场景决策树
javascript
是否需要返回响应式数据?
├─ 是 → 使用 computed
└─ 否 → 继续判断
是否需要访问旧值?
├─ 是 → 使用 watch
└─ 否 → 继续判断
是否需要精确控制监听的依赖?
├─ 是 → 使用 watch(显式指定源)
└─ 否 → 使用 watchEffect(自动追踪)
是否需要立即执行副作用?
├─ 是 → 使用 watchEffect 或 watch + immediate
└─ 否 → 使用 watch(默认懒执行)
是否需要深度监听嵌套对象?
├─ 是 → 使用 watchEffect(自动深度)或 watch + deep
└─ 否 → 都可以
典型使用场景示例
1. computed 场景
html
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Zhang')
const lastName = ref('San')
// 场景1:拼接姓名
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 场景2:购物车总价计算(有缓存,只在商品变化时重新计算)
const cartItems = ref([
{ name: '商品1', price: 100, count: 2 },
{ name: '商品2', price: 200, count: 1 }
])
const totalPrice = computed(() => {
return cartItems.value.reduce((sum, item) => sum + item.price * item.count, 0)
})
// 场景3:表单验证状态
const username = ref('')
const isValid = computed(() => username.value.length >= 3)
</script>
2. watch 场景
html
<script setup>
import { ref, watch } from 'vue'
const searchKeyword = ref('')
const searchResults = ref([])
// 场景1:监听单个源,执行异步操作(防抖)
let timer
watch(searchKeyword, (newVal, oldVal) => {
clearTimeout(timer)
timer = setTimeout(async () => {
if (newVal) {
searchResults.value = await fetchSearchResults(newVal)
}
}, 500)
})
// 场景2:监听多个源
const page = ref(1)
const pageSize = ref(10)
watch([page, pageSize], ([newPage, newSize], [oldPage, oldSize]) => {
console.log(`页码从 ${oldPage} 变为 ${newPage},每页条数从 ${oldSize} 变为 ${newSize}`)
fetchData()
})
// 场景3:深度监听对象
const userInfo = ref({ name: '张三', address: { city: '北京' } })
watch(userInfo, (newVal) => {
console.log('用户信息变化', newVal)
}, { deep: true, immediate: true })
</script>
3. watchEffect 场景
html
<script setup>
import { ref, watchEffect } from 'vue'
const userId = ref('')
const userData = ref(null)
const loading = ref(false)
// 场景1:自动追踪依赖,立即执行
watchEffect(async () => {
if (userId.value) {
loading.value = true
try {
// 自动追踪 userId 和 loading
userData.value = await fetchUser(userId.value)
} finally {
loading.value = false
}
}
})
// 场景2:清理副作用(取消请求)
watchEffect((onCleanup) => {
let cancelled = false
onCleanup(() => {
cancelled = true
console.log('清理副作用')
})
fetchData().then(data => {
if (!cancelled) {
// 更新数据
}
})
})
// 场景3:控制刷新时机(DOM更新后执行)
watchEffect(() => {
// 操作DOM,在flush: 'post'时确保DOM已更新
const element = document.getElementById('content')
console.log(element?.innerHTML)
}, { flush: 'post' })
</script>
性能与最佳实践建议
| 场景 | 推荐API | 原因 |
|---|---|---|
| 模板中使用的派生数据 | computed | 有缓存,避免模板重复计算 |
| 执行异步操作(API请求) | watch 或 watchEffect | 需要控制执行时机和防抖 |
| 需要精确控制监听源 | watch | 显式指定依赖,更可控 |
| 多个依赖的副作用 | watchEffect | 自动追踪,代码简洁 |
| 性能敏感场景(高频触发) | computed | 缓存机制减少计算 |
| 表单联动验证 | computed | 返回验证状态,适合模板绑定 |
| 路由参数变化响应 | watch | 需要访问旧值进行对比 |
| DOM操作(访问更新后的DOM) | watchEffect + flush: 'post' | 确保DOM已更新 |
核心要点总结
-
computed 用于派生数据:有缓存、懒计算、只读,适合模板中需要展示的计算结果
-
watch 用于精确监听:显式指定源、可访问旧值、可控性强,适合执行异步操作或复杂副作用
-
watchEffect 用于自动追踪:简化写法、立即执行、自动深度监听,适合不需要旧值的副作用场景
-
三者都能停止监听:组件卸载时自动停止,手动调用返回函数可提前停止
-
flush 选项控制执行时机:
-
pre:组件更新前(默认) -
post:组件更新后(可访问DOM) -
sync:同步执行(不推荐,可能影响性能)
-