返回博客

vue-router 3.0 的 .catch() 会无声吞掉后续所有代码

某后台管理系统有一个数据筛选面板,用户点击时间范围按钮(今日 / 本周 / 本月 / 上月),UI 高亮切换确实生效,但父组件完全收不到筛选变化——页面数据纹丝不动,网络面板零请求。重新渲染、调 forceUpdate、反复排查父子传参……折腾了一圈,控制台里其实早就有一行报错,只是太容易被忽视。

现象

  • 点击筛选按钮,组件内部状态(activeRange)正确更新,按钮高亮正常
  • 父组件监听的 @filter-change 事件从不触发,下游 API 请求为零
  • 控制台出现:TypeError: Cannot read properties of undefined (reading 'catch') in a v-on handler
  • 错误信息里没有任何关于 $emit 或业务逻辑的字样,极易被当成"无关警告"跳过

根因

vue-router 在 3.1.0 版本才让 push() / replace() 返回 Promise。3.0.x 及以下这两个方法返回的是 undefined

问题代码长这样:

onFilterChange() {
  const params = this.getFilterParams()
  // vue-router 3.0.x 的 replace() 返回 undefined
  // undefined.catch() 直接抛出 TypeError
  this.$router.replace({ query: { ...params } }).catch(() => {})
  // ↑ 上面一行已经 throw,以下代码永远不执行
  this.$emit('filter-change', params)
}

vue-element-admin 4.x 模板默认依赖 vue-router 3.0.x。很多人(包括官方文档示例)会给路由调用加 .catch(() => {}) 来屏蔽 NavigationDuplicated 错误——这个模式在 vue-router >= 3.1 完全正确,但在 3.0.x 上是一颗定时炸弹。

坑:undefined.catch() 会同步 throw,后面的 $emit 成为死代码

vue-router 3.0.x 的 push/replace 返回 undefined,链式调用 .catch() 会立即抛出 TypeError。JavaScript 的同步 throw 会中断整个函数执行,方法里位于 .catch() 之后的所有代码——包括 $emit、状态更新、日志——一行都不会跑。

这个坑的恶意之处在于:UI 交互(按钮高亮)源自组件内部响应式数据,不依赖 $emit,所以看起来"正常运行了",但父子通信已经彻底断开。

正解

方案 A(推荐):业务逻辑先执行,路由同步放后面

onFilterChange() {
  const params = this.getFilterParams()
  // 先 emit,业务逻辑不依赖路由同步是否成功
  this.$emit('filter-change', params)
  // 再同步 URL,守卫 .catch()
  const result = this.$router.replace({
    query: { ...this.$route.query, ...params }
  })
  if (result && result.catch) result.catch(() => {})
}

方案 B:全局 monkey-patch,一次性解决所有路由调用

// router/index.js
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}
const originalReplace = VueRouter.prototype.replace
VueRouter.prototype.replace = function replace(location) {
  return originalReplace.call(this, location).catch(err => err)
}

方案 B 改一处、全项目受益,适合已经大量散落 .catch() 调用的老项目。方案 A 是更好的设计原则——业务事件($emit)不应该依赖路由 URL 同步成功,两件事本就是独立的。

正解:emit 和路由同步是两件独立的事,别让路由 throw 绑架业务逻辑

$emit 通知父组件是业务职责,$router.replace 同步 URL 是 UX 附加项。前者失败会导致功能断裂,后者失败顶多 URL 不好看。把 $emit 提前到路由调用之前,或者全局 patch 让路由始终返回 Promise,都能彻底解决这个问题。

排查方法

  1. 打开 DevTools Network 面板,点击触发按钮,看有没有 API 请求
  2. 看 Console 里有没有 TypeError: Cannot read properties of undefined (reading 'catch')
  3. 定位到报错所在方法,检查 $router.push/replace 后面是否接了 .catch()
  4. 确认 package.jsonvue-router 版本——< 3.1.0 则命中此坑

一句话外卖

vue-router 3.0.x 的 push/replace 不返回 Promise,给它链式 .catch() 等于在方法里埋了一个同步炸弹,炸掉之后的所有代码包括 $emit