响应式调试(onTrack/onTrigger)
响应式调试(onTrack/onTrigger)
Vue 3 的响应式系统提供了两个强大的调试钩子:onTrack
和 onTrigger
。它们允许开发者观察响应式依赖的收集和触发过程,对于理解复杂的响应式行为或排查性能问题特别有用。
基本概念
onTrack
和 onTrigger
是 effect
的选项,当响应式依赖被追踪或触发时会被调用:
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
: 被触发的 effecttarget
: 被修改的响应式对象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
}
})
}
响应式调试的限制
- 生产环境应禁用这些调试钩子,它们会影响性能
- 对于大型应用,会产生大量调试日志
- 不能直接调试 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