Vue Object Prop 没有 default,$emit 被悄悄截断
某天在维护一个后台管理系统时,发现一个列表页的搜索按钮点了没反应——表格纹丝不动,分页点了也不刷新,排序也失效。但是翻遍页面,没有任何报错弹窗,UI 交互也看起来正常:输入框可以输字,按钮有水波纹点击效果。
打开 DevTools,才发现控制台角落里躺着一行小小的警告,极易被忽略:
[Vue warn]: Error in v-on handler: "TypeError: Cannot set properties of undefined (setting 'query')"
现象
父组件正确绑定了基础组件的 @on-change 事件,却从未收到回调。具体表现:
- 搜索:输入关键词点查询,表格不刷新
- 分页:点第 2 页,列表不重载
- 排序:点列头,顺序不变化
其他页面复用同一个基础组件,功能完全正常。唯独这一个消费页面失效,而且没有任何可见的错误提示。
根因
基础组件在 props 中声明了一个 Object 类型的 prop,但没有设置 default:
// BaseTable.vue(基础组件)
props: {
routeData: {}, // ⚠️ 没有 type,没有 default
}失效的消费页面碰巧没有传这个 prop:
<!-- 消费页面:忘了传 :route-data -->
<base-table @on-change="getData" />于是 this.routeData 是 undefined。基础组件的内部方法里有这样一段:
dispatch() {
var vars = this.makeData()
this.routeData.query = vars // 💥 undefined.query = vars → TypeError
this.$router.push(this.routeData)
this.$emit('on-change', vars) // ⛔ 永远跑不到这里
}undefined.query = vars 直接抛出 TypeError,方法中止,$emit 成了死代码。父组件的 @on-change="getData" 永远收不到事件,整个功能全线瘫痪。
Vue 对 prop 缺失的处理是赋值 undefined,而不是报错。TypeError 发生在 Vue 的事件处理器内部,只产生一条 [Vue warn] 警告,不会触发 app 级别的错误边界或 toast,极易被忽视。更危险的是:其他正常传递该 prop 的消费页面完全不受影响,组件"看起来没问题",让定位变得更难。
正解
修复分两步,缺一不可。
第一步:给 Object/Array prop 声明 type + default 工厂函数
props: {
routeData: {
type: Object,
default: () => ({}), // ✅ 永远不是 undefined
},
}注意:Vue 2 和 Vue 3 中,Object / Array 类型的 default 必须是返回值的函数,不能写 default: {}。后者会让所有组件实例共享同一个对象引用,引发另一个响应式污染 bug。
第二步:解引用加守卫,但 $emit 放在守卫外
dispatch() {
var vars = this.makeData()
if (this.routeData && this.routeData.name) {
this.routeData.query = vars
this.$router.push(this.routeData)
}
this.$emit('on-change', vars) // ✅ 守卫之外,始终执行
}守卫只包裹解引用和 router push 这两行可选逻辑,$emit 保持在守卫外部。这样:
- 传了
routeData的消费页:完整功能,包含 URL 同步 - 没传
routeData的消费页:核心搜索/分页依然正常,只丢失 URL 恢复能力
如果把 $emit 也圈进守卫里,忘传 prop 的消费页连核心功能都会静默失效,爆炸半径由基础组件作者决定,应该选择"尽量宽容"的降级策略。
三件事同时做:default: () => ({}) 防止 undefined、if (this.prop && this.prop.key) 守卫每个解引用赋值、$emit 放在守卫外保证事件链完整。
排查速查
遇到"父组件绑了 @event,但回调从没触发"时,按这个顺序查:
- 基础组件
props里每个 Object/Array prop 是否都有default: () => (...)? - 方法或生命周期里是否有
this.<prop>.<key> = value的直接赋值? - 消费页模板里是否真的传了
:<prop-name>? - 在
$emit前一行加console.log('emit fires', vars)——如果没有打印,说明方法在此之前就已经中止。
开启 ESLint 的 vue/require-default-prop + vue/require-prop-types 规则可以在 lint 阶段拦截这类问题,推荐写进项目 .eslintrc。
一句话外卖
Vue 基础组件的 Object/Array prop 没有 default,任何一个消费页忘传,就会在组件内部无声截断整条 emit 链——加
default: () => ({})是防御性编程的基本动作。