响应式调试
遇到组件意外更新、计算属性未按预期失效或 watcher 的依赖不明确时,可以使用开发态调试 Hook 查看依赖来源,并在变化通知发生时用 debugger 暂停,结合 sourcemap 找到业务赋值行。
本页示例同时适用于小程序和 Mpx2RN。Web 输出使用 Vue 2.7 的原生调试接口。所有调试 Hook 和 onTrack/onTrigger 仅在开发环境生效,生产环境保持原响应式与调度行为。
调试选项也支持选项式用法:页面和组件的 watch 可在 handler 对象中配置 onTrack/onTrigger,this.$watch 可通过第三参数或第二参数的 handler 对象传入。选项式 computed 没有独立的调试配置入口,需要使用组合式 computed 的第二参数。
组件级渲染调试同样支持选项式 renderTracked / renderTriggered,可直接声明在页面或组件选项中,无需 setup;支持 Mixin 合并,钩子中的 this 指向当前实例。
观察读取与通知
onRenderTracked回答“本次组件渲染读取了哪个响应式依赖”。onRenderTriggered回答“哪个依赖的变化通知了当前组件的渲染 Effect”。computed的第二参数以及 watch 系列的 options 中,可以使用onTrack/onTrigger单独观察对应的计算或侦听。
trigger 回调同步执行,位于原 update/scheduler 之前。这时业务赋值的调用栈通常仍在,适合定位写入位置。后续任务仍按原 scheduler 执行,多个 trigger 可以合并到一次渲染。Hook 不等待 setData 回调或 React 提交。
在业务组件中设置断点
将下面的组件放进目标页面,并使用开发构建运行。组合式组件 Hook 必须在 setup 同步注册;使用 <script setup> 时也须在顶层同步调用,并按平台要求暴露模板使用的状态。
<template>
<view bindtap="increase">
<text>点击加一:{{ count }},两倍:{{ doubled }}</text>
</view>
</template>
<script>
import {
createComponent,
ref,
computed,
onRenderTracked,
onRenderTriggered
} from '@mpxjs/core'
createComponent({
setup () {
const count = ref(0)
const doubled = computed(() => count.value * 2)
if (process.env.NODE_ENV !== 'production') {
onRenderTracked((event) => {
if (event.target === count || event.target === doubled) {
console.log('render tracked', event.type, event.key)
}
})
onRenderTriggered((event) => {
if (event.target === count && event.key === 'value') {
// eslint-disable-next-line no-debugger
debugger
}
})
}
return {
count,
doubled,
increase () {
count.value++
}
}
}
})
</script>
<script type="application/json">
{
"component": true
}
</script>进入页面后,控制台会输出组件读取的依赖。点击时在 onRenderTriggered 中暂停,检查 event.target === count、event.type === 'set'、event.key === 'value' 以及 oldValue/newValue。在调用栈中选择 increase() 所在的业务帧,即可查看原始赋值行和局部变量;继续执行后组件按原调度规则更新。
Mpx 普通 ref 的调试 target 是业务持有的 ref,所以可以直接按身份筛选。toRef(state, 'count') 转发到源属性时,target/key 则通常是 state/count;不要把所有事件都按 ref 身份过滤。
调试回调应只筛选、查看和暂停。小程序和 Mpx2RN 会隔离调试回调的依赖收集,普通响应式读取不会成为当前 Effect 的额外依赖。Web 的 watch/computed 使用 Vue 2.7 原生实现,不提供同样的读取隔离;尤其不要在 onTrack 中读取 ref.value 或其他响应式属性,否则可能递归触发 onTrack。动态过滤条件和断点开关应使用普通变量或普通对象,像上例一样检查 event 字段和对象身份即可。各平台均应避免在回调中写状态、运行副作用或调用有副作用的 getter。
使用 sourcemap 找到业务源码
先在业务赋值行设置普通断点,确认开发者工具已经能定位原始 .mpx/.ts,再进入响应式 Hook。Webpack 配置可以开启完整 source map,合并到项目现有配置中:
{
mode: 'development',
devtool: 'source-map'
}排查步骤如下:
- 使用同一次构建生成的 bundle 和 map,并确认它们对应当前运行平台。
- 在小程序开发者工具或 RN JavaScript 调试器中启用源码映射,加载包含 Hook 的组件。
- 复现一次操作,在同步 onRenderTriggered 断点处暂停。
- 调用栈会包含业务方法、ref/属性 setter、Dep.notify 和调试分发代码;选择映射后的第一个相关业务帧。
- 检查写入的来源与当时上下文,然后继续执行观察实际更新。
只定位业务赋值时,先验证业务 script 的映射即可。需要单步进入 Mpx 的 Dep/Effect 时,最终 map 还应保留 @mpxjs/core 源码位置,并检查调试器是否忽略了 node_modules。
RN 使用 Hermes 字节码产物时,需要对应产物的最终映射;不能使用另一平台或另一份未组合的 JS map。Mpx 调试 Hook 不自动解析、生成或上传 sourcemap。
模板生成的 render 代码也需要自己的映射。业务 script 的 map 可用,不代表模板表达式已有精确行列;本页的优先定位目标是产生状态写入的业务脚本。
单独排查 computed
import { ref, computed } from '@mpxjs/core'
const count = ref(0)
const doubled = computed(() => count.value * 2, {
onTrack(event) {
console.log('computed tracked', event.type, event.key)
},
onTrigger(event) {
if (event.target === count) debugger
}
})
console.log(doubled.value) // 首次读取收集 count.value
count.value++ // 同步通知,随后按原逻辑标记 dirtycomputed 读取时仍使用原缓存规则。onTrigger 在原 scheduler 标记 dirty 之前调用,不能假设 dirty 已更新,更不能据此认定 computed 的结果已经变化。
Mpx 的依赖转交沿用 Vue 2.7 式语义。如果组件只读取 doubled.value,可以看到这样的事件:
| 接口 | target / key |
|---|---|
| 组件 onRenderTracked | doubled / value |
| computed onTrack | count / value |
| count 改变时的 computed onTrigger | count / value |
| count 改变时的组件 onRenderTriggered | count / value |
组件 trigger 的来源是底层 count。这种情况无需在 doubled 上新增赋值。
单独排查 watch
import { ref, watch } from '@mpxjs/core'
const count = ref(0)
const stop = watch(count, (value, oldValue) => {
console.log('watch callback', oldValue, value)
}, {
onTrack(event) {
console.log('watch tracked', event.type, event.key)
},
onTrigger(event) {
if (event.target === count) debugger
}
})
count.value++ // 先同步 onTrigger,watch callback 仍按 flush 配置执行
stop()watchEffect、watchSyncEffect、watchPostEffect 同样支持这两个 options。onTrack 对应 watcher getter 的依赖收集,onTrigger 对应依赖通知;重新运行后的 getter 值没有变化时,watch callback 仍可能被跳过。停止 watcher 后正常取消订阅,调试事件也停止。
在小程序和 Mpx2RN 中,flush 配置控制 Mpx 的 JS 调度队列。后置回调不能单独作为原生视图或 React 提交完成的保证;需要判断视图完成时,使用平台对应的完成回调或生命周期。
看懂数组事件与更新次数
import { reactive, watch } from '@mpxjs/core'
const state = reactive({ items: [] })
watch(() => state.items, () => {}, {
onTrigger(event) {
console.log(event.type, event.key)
}
})
state.items.push('first') // array mutation / push
state.items.splice(0, 1, 'second') // array mutation / splice数组事件以数组自身为 target、方法名为 key,不创建数组快照。直接新增对象属性和直接数组索引赋值仍遵循 Mpx 的既有响应式限制;应按实际需求使用 set、del、splice 等已有 API。
相同属性被重复读取时可能多次 track,但订阅依然去重。一次赋值可能通知多个 Effect,连续赋值可能合并为一次组件更新,所以调试事件次数不等于渲染次数。
Mpx2RN 的组件还可以由父组件、React Hooks 或其他 React 输入驱动渲染,这些路径可能只有 track,没有 Mpx 响应式 trigger。React 开发态重复执行渲染,也不等于重复提交了屏幕画面。
常见排查结果
| 现象 | 检查方向 |
|---|---|
| 没有 render track | 开发环境、setup 同步注册、当前渲染是否实际读取状态,以及是否创建 Mpx 渲染 Effect |
| 值变了却没有 trigger | 同值赋值、stop/unmount、未被观察的新增属性或数组索引、过滤的 target 是否正确 |
| computed track 是 doubled,trigger 是 count | Vue 2.7 式底层依赖转交的正常现象 |
| Hook 能停,但业务源码行不对 | bundle/map 是否匹配、script loader 映射链、调试器的 source map 与忽略列表设置 |
| 打印的旧对象后来也变了 | oldValue/newValue 是对象引用,控制台稍后展开时可能展示后续状态 |
| RN 有 track 没有 trigger | 检查父级、React Hooks 等 React 输入 |
生产环境忽略框架调试接口;业务主动写入的普通日志或其他位置的 debugger 不会由框架自动删除。建议像完整示例一样用开发态条件包裹临时调试代码。