阿里云主机折上折
  • 微信号
Current Site:Index > Responsive debugging (onTrack/onTrigger)

Responsive debugging (onTrack/onTrigger)

Author:Chuan Chen 阅读数:54009人阅读 分类: Vue.js

Responsive Debugging (onTrack/onTrigger)

Vue 3's reactivity system provides two powerful debugging hooks: onTrack and onTrigger. They allow developers to observe the collection and triggering of reactive dependencies, which is particularly useful for understanding complex reactive behaviors or troubleshooting performance issues.

Basic Concepts

onTrack and onTrigger are options for effect that are called when reactive dependencies are tracked or triggered:

import { reactive, effect } from 'vue'

const state = reactive({ count: 0 })

effect(
  () => {
    console.log(state.count)
  },
  {
    onTrack(e) {
      console.log('Dependency tracked', e)
    },
    onTrigger(e) {
      console.log('Dependency triggered', e)
    }
  }
)

onTrack Explained

When the effect runs for the first time and accesses a reactive property, onTrack is called. It receives an event object containing the following key information:

  • effect: The currently running effect function
  • target: The reactive object being accessed
  • type: The type of operation ('get' | 'has' | 'iterate')
  • key: The property key being accessed
const user = reactive({
  name: 'Alice',
  age: 25
})

effect(
  () => {
    const info = `${user.name} (${user.age})`
  },
  {
    onTrack({ effect, target, type, key }) {
      console.log(`Tracked ${type} operation on property ${String(key)}`)
    }
  }
)
// Output:
// Tracked get operation on property name
// Tracked get operation on property age

onTrigger Explained

When a reactive property is modified and triggers the effect to re-run, onTrigger is called. The event object contains:

  • effect: The triggered effect
  • target: The modified reactive object
  • type: The type of operation ('set' | 'add' | 'delete')
  • key: The modified property key
  • newValue: The new value
  • oldValue: The old value
const cart = reactive({
  items: [],
  total: 0
})

effect(
  () => {
    console.log('Cart total:', cart.total)
  },
  {
    onTrigger({ key, newValue, oldValue }) {
      console.log(`Total changed from ${oldValue} to ${newValue}`)
    }
  }
)

cart.total = 100
// Output:
// Total changed from 0 to 100
// Cart total: 100

Practical Use Cases

Debugging Computed Properties

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:', total.value)
  },
  {
    onTrack(e) {
      console.log('Computed property dependency tracked:', e.key)
    },
    onTrigger(e) {
      console.log('Computed property update triggered:', e.key, e.newValue)
    }
  }
)

product.price = 200 // Will trigger onTrack and onTrigger

Observing Deep Reactive Objects

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

effect(
  () => {
    console.log(nested.user.profile.preferences.theme)
  },
  {
    onTrack({ target, key }) {
      console.log('Tracked path:', target, key)
    }
  }
)

nested.user.profile.preferences.theme = 'light'
// Will display the complete property access path

Performance Optimization Tips

Use onTrigger to identify unnecessary effect triggers:

const config = reactive({
  theme: 'dark',
  version: '1.0.0' // This property won't change
})

effect(
  () => {
    // Only uses theme
    document.body.className = config.theme
  },
  {
    onTrigger({ key }) {
      if (key === 'version') {
        console.warn('version changed but not used, may cause unnecessary re-rendering')
      }
    }
  }
)

config.version = '1.0.1' // Will trigger a warning

Using with watch

import { watch, reactive } from 'vue'

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

watch(
  () => state.loading,
  (newVal) => {
    console.log('Loading state changed:', newVal)
  },
  {
    onTrack({ target, key }) {
      console.log('Watch dependency tracked:', key)
    },
    onTrigger({ key, oldValue, newValue }) {
      console.log(`Watch triggered: ${key} changed from ${oldValue} to ${newValue}`)
    }
  }
)

state.loading = true
// Output:
// Watch dependency tracked: loading
// Watch triggered: loading changed from false to true
// Loading state changed: true

Debugging Component State

Using reactive debugging in components:

import { onRenderTracked, onRenderTriggered } from 'vue'

export default {
  setup() {
    onRenderTracked((e) => {
      console.log('Render dependency tracked:', e)
    })

    onRenderTriggered((e) => {
      console.log('Render triggered:', e)
    })

    return {
      // Reactive data
    }
  }
}

Advanced Usage: Custom Debugging Tools

You can build reactive debugging tools based on these hooks:

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

Limitations of Reactive Debugging

  1. These debugging hooks should be disabled in production as they impact performance.
  2. For large applications, they can generate excessive debug logs.
  3. Cannot directly debug the internal reactivity of state management systems like Vuex/Pinia.

Integration with Other Debugging Tools

Can be used alongside Vue Devtools:

// Register global debugging hooks in development environment
if (process.env.NODE_ENV === 'development') {
  let isDebugging = false
  
  const toggleDebug = () => {
    isDebugging = !isDebugging
    if (isDebugging) {
      window.__VUE_DEBUG_HOOKS__ = {
        onTrack: ({ key }) => console.log('Global tracking:', key),
        onTrigger: ({ key }) => console.log('Global triggering:', key)
      }
    } else {
      delete window.__VUE_DEBUG_HOOKS__
    }
  }
  
  // Can be called via console with toggleDebug()
  window.toggleVueDebug = toggleDebug
}

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

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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.