Vue3 响应式系统核心对比:effect, track, trigger,computed, watch, watchEffect

Vue3响应式系统核心对比


effect/track/trigger机制:

  • effect负责执行副作用
  • track在数据读取时建立依赖
  • trigger在数据修改时触发更新

三者协作实现响应式闭环


computed/watch/watchEffect对比:

  • computed:有缓存的派生数据计算,适合模板展示
  • watch:精确监听特定源,支持异步和旧值访问
  • watchEffect:自动追踪依赖,立即执行副作用

性能优化:通过Proxy拦截、依赖清理和调度控制实现高效更新,computed的缓存机制减少重复计算


使用场景:根据是否需要返回值、旧值访问和精确控制依赖选择不同API,组件卸载时自动停止监听


以下是 Vue3 响应式系统中 effecttracktrigger 三个核心函数的对比总结:


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
  }
}

关键要点总结

  1. effect是订阅者:负责注册和执行副作用函数

  2. track是订阅过程:在数据被读取时,将当前effect订阅到该数据的依赖列表中

  3. trigger是发布过程:在数据被修改时,找出所有订阅了该数据的effect并执行

  4. 三者共同实现了观察者模式:effect是观察者,响应式数据是被观察者,track和trigger是连接两者的桥梁

  5. 性能优化:通过Proxy拦截、依赖清理、调度器控制等方式,实现了精确的依赖追踪和高效的更新触发


以下是 Vue3 中 computedwatchwatchEffect 三个响应式 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请求) watchwatchEffect 需要控制执行时机和防抖
需要精确控制监听源 watch 显式指定依赖,更可控
多个依赖的副作用 watchEffect 自动追踪,代码简洁
性能敏感场景(高频触发) computed 缓存机制减少计算
表单联动验证 computed 返回验证状态,适合模板绑定
路由参数变化响应 watch 需要访问旧值进行对比
DOM操作(访问更新后的DOM) watchEffect + flush: 'post' 确保DOM已更新

核心要点总结

  1. computed 用于派生数据:有缓存、懒计算、只读,适合模板中需要展示的计算结果

  2. watch 用于精确监听:显式指定源、可访问旧值、可控性强,适合执行异步操作或复杂副作用

  3. watchEffect 用于自动追踪:简化写法、立即执行、自动深度监听,适合不需要旧值的副作用场景

  4. 三者都能停止监听:组件卸载时自动停止,手动调用返回函数可提前停止

  5. flush 选项控制执行时机

    • pre:组件更新前(默认)

    • post:组件更新后(可访问DOM)

    • sync:同步执行(不推荐,可能影响性能)

相关推荐
saadiya~2 小时前
从插件冗余到极致流畅:我的 Vue 3 开发环境“瘦身”实录
前端·javascript·vue.js
慧一居士2 小时前
Zod 功能、使用场景介绍以及对应场景使用示例
前端·vue.js
Irene19913 小时前
Vue3 举例说明如何编写一个自定义组合式函数(与 Mixins 相比的优势)
vue.js
小马_xiaoen3 小时前
Vue 3 + TS 实战:手写 v-no-emoji 自定义指令,彻底禁止输入框表情符号!
前端·javascript·vue.js
Highcharts.js3 小时前
Highcharts Gantt 实战:从框架集成到高级功能应用-打造现代化、交互式项目进度管理图表
前端·javascript·vue.js·信息可视化·免费
终端鹿3 小时前
setup 语法糖从 0 到 1 实战教程
前端·javascript·vue.js
英俊潇洒美少年3 小时前
Vue3 中使用 Proxy 的 8 个注意事项
vue.js
炒毛豆4 小时前
Vue 3 公共组件从封装到全局注册的极简指南
前端·javascript·vue.js
踩着两条虫4 小时前
VTJ.PRO 在线应用开发平台前端架构
前端·vue.js·ai编程