阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 响应式调试(onTrack/onTrigger)

响应式调试(onTrack/onTrigger)

作者:陈川 阅读数:54010人阅读 分类: Vue.js

响应式调试(onTrack/onTrigger)

Vue 3 的响应式系统提供了两个强大的调试钩子:onTrackonTrigger。它们允许开发者观察响应式依赖的收集和触发过程,对于理解复杂的响应式行为或排查性能问题特别有用。

基本概念

onTrackonTriggereffect 的选项,当响应式依赖被追踪或触发时会被调用:

import { reactive, effect } from 'vue'

const state = reactive({ count: 0 })

effect(
  () => {
    console.log(state.count)
  },
  {
    onTrack(e) {
      console.log('依赖被追踪', e)
    },
    onTrigger(e) {
      console.log('依赖被触发', e)
    }
  }
)

onTrack 详解

当 effect 首次运行并访问响应式属性时,onTrack 会被调用。它接收一个事件对象,包含以下关键信息:

  • effect: 当前运行的 effect 函数
  • target: 被访问的响应式对象
  • type: 操作类型 ('get' | 'has' | 'iterate')
  • key: 被访问的属性键
const user = reactive({
  name: 'Alice',
  age: 25
})

effect(
  () => {
    const info = `${user.name} (${user.age})`
  },
  {
    onTrack({ effect, target, type, key }) {
      console.log(`追踪到 ${String(key)} 属性的 ${type} 操作`)
    }
  }
)
// 输出:
// 追踪到 name 属性的 get 操作
// 追踪到 age 属性的 get 操作

onTrigger 详解

当响应式属性被修改并触发 effect 重新运行时,onTrigger 会被调用。事件对象包含:

  • effect: 被触发的 effect
  • target: 被修改的响应式对象
  • type: 操作类型 ('set' | 'add' | 'delete')
  • key: 被修改的属性键
  • newValue: 新值
  • oldValue: 旧值
const cart = reactive({
  items: [],
  total: 0
})

effect(
  () => {
    console.log('购物车总价:', cart.total)
  },
  {
    onTrigger({ key, newValue, oldValue }) {
      console.log(`总价从 ${oldValue} 变更为 ${newValue}`)
    }
  }
)

cart.total = 100
// 输出:
// 总价从 0 变更为 100
// 购物车总价: 100

实际应用场景

调试计算属性

import { computed, reactive } from 'vue'

const product = reactive({
  price: 100,
  quantity: 2
})

const total = computed({
  get() {
    return product.price * product.quantity
  },
  set(value) {
    product.quantity = value / product.price
  }
})

effect(
  () => {
    console.log('总价:', total.value)
  },
  {
    onTrack(e) {
      console.log('计算属性依赖追踪:', e.key)
    },
    onTrigger(e) {
      console.log('计算属性触发更新:', e.key, e.newValue)
    }
  }
)

product.price = 200 // 会触发 onTrack 和 onTrigger

观察深层响应式对象

const nested = reactive({
  user: {
    profile: {
      name: 'Bob',
      preferences: {
        theme: 'dark'
      }
    }
  }
})

effect(
  () => {
    console.log(nested.user.profile.preferences.theme)
  },
  {
    onTrack({ target, key }) {
      console.log('追踪路径:', target, key)
    }
  }
)

nested.user.profile.preferences.theme = 'light'
// 会显示完整的属性访问路径

性能优化提示

通过 onTrigger 可以识别不必要的 effect 触发:

const config = reactive({
  theme: 'dark',
  version: '1.0.0' // 这个属性不会改变
})

effect(
  () => {
    // 只使用 theme
    document.body.className = config.theme
  },
  {
    onTrigger({ key }) {
      if (key === 'version') {
        console.warn('version 改变但未被使用,可能造成不必要的重渲染')
      }
    }
  }
)

config.version = '1.0.1' // 会触发警告

与 watch 结合使用

import { watch, reactive } from 'vue'

const state = reactive({
  loading: false,
  data: null
})

watch(
  () => state.loading,
  (newVal) => {
    console.log('loading 状态变化:', newVal)
  },
  {
    onTrack({ target, key }) {
      console.log('watch 依赖被追踪:', key)
    },
    onTrigger({ key, oldValue, newValue }) {
      console.log(`watch 触发: ${key} 从 ${oldValue} 变为 ${newValue}`)
    }
  }
)

state.loading = true
// 输出:
// watch 依赖被追踪: loading
// watch 触发: loading 从 false 变为 true
// loading 状态变化: true

调试组件状态

在组件中使用响应式调试:

import { onRenderTracked, onRenderTriggered } from 'vue'

export default {
  setup() {
    onRenderTracked((e) => {
      console.log('渲染依赖追踪:', e)
    })

    onRenderTriggered((e) => {
      console.log('渲染触发:', e)
    })

    return {
      // 响应式数据
    }
  }
}

高级用法:自定义调试工具

可以基于这些钩子构建响应式调试工具:

let activeEffect = null
const effectStack = []
const dependencyMap = new WeakMap()

function trackEffect(effect, target, type, key) {
  if (!dependencyMap.has(target)) {
    dependencyMap.set(target, new Map())
  }
  const depsMap = dependencyMap.get(target)
  if (!depsMap.has(key)) {
    depsMap.set(key, new Set())
  }
  const dep = depsMap.get(key)
  dep.add(effect)
}

function createEffect(fn, options = {}) {
  const effect = () => {
    try {
      effectStack.push(effect)
      activeEffect = effect
      return fn()
    } finally {
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1]
    }
  }
  effect.options = options
  effect()
  return effect
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      if (activeEffect) {
        trackEffect(activeEffect, target, 'get', key)
        activeEffect.options.onTrack?.({
          effect: activeEffect,
          target,
          type: 'get',
          key
        })
      }
      return target[key]
    },
    set(target, key, value) {
      const oldValue = target[key]
      target[key] = value
      const depsMap = dependencyMap.get(target)
      if (depsMap && depsMap.has(key)) {
        depsMap.get(key).forEach((effect) => {
          effect.options.onTrigger?.({
            effect,
            target,
            type: 'set',
            key,
            newValue: value,
            oldValue
          })
          effect()
        })
      }
      return true
    }
  })
}

响应式调试的限制

  1. 生产环境应禁用这些调试钩子,它们会影响性能
  2. 对于大型应用,会产生大量调试日志
  3. 不能直接调试 Vuex/Pinia 等状态管理的内部响应式

与其他调试工具结合

可以配合 Vue Devtools 使用:

// 在开发环境中注册全局调试钩子
if (process.env.NODE_ENV === 'development') {
  let isDebugging = false
  
  const toggleDebug = () => {
    isDebugging = !isDebugging
    if (isDebugging) {
      window.__VUE_DEBUG_HOOKS__ = {
        onTrack: ({ key }) => console.log('全局追踪:', key),
        onTrigger: ({ key }) => console.log('全局触发:', key)
      }
    } else {
      delete window.__VUE_DEBUG_HOOKS__
    }
  }
  
  // 可以通过控制台调用 toggleDebug()
  window.toggleVueDebug = toggleDebug
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌