VueRouter重复点击报错原因深度解析

625 阅读3分钟

前言:最近在做一个vue项目的时候遇到一个bug:重复点击导航组件中的某个按钮,控制台会报错。虽然并不会产生什么实际影响(目前看来是这样),但是打开控制台看着它就碍眼,所以在网上查了一下问题的原因,但是网上没有具体原因的阐述,只给出了解决方案。本文试图对其原因进行深度探究,想直接看解决方案的直接跳到解决办法

bug具体情况

//template部分
    <span " @click="goTo('/search')"></span>
//跳转方法
  methods:{
    goTo(path){
      this.$router.replace(path)
    }
  }

当重复点击该span时,控制台会报一个error:

image.png

error分析

我们从错误信息可以看出该error的信息:Avoided redundant navigation to current location:(避免对当前位置的冗余导航),并且知道这个error跟promise有关。

从网上的解决方案开始探索

网上给出的解决方法主要有这个几个:

  1. 将vue router版本降低到3.0
  2. 在路由器index.js中加入如下代码
// 以push为例,replace同理
const routerPush = Router.prototype.push
Router.prototype.push = function push(location) {
  return routerPush.call(this, location).catch(error=> error)

3.补齐push方法的参数:

this.$router.push(route, () => {}, (e) => {
    console.log('输出报错',e) 
})

然而网上的信息也仅限于此了,并没有解释出现这个问题的原因。从这两个解决办法我们可以获取到两个信息:首先,vue router3.0是没有这个bug的;其次,给push方法添加catch后该bug即解决。结合上个section得到的信息,我们更加确定这个bug是由于promise未添加catch导致的。接下来我们就查看一下vue router的源码,看看push具体是怎么实现的。

vue router 源码探索

本来想直接看源码,结果遇到了小挫折:源码是ts写的。。。然后想到可以把ts转成js,奈何还是没有找到想要的。这时想到一个巧妙的办法:在路由器的index.js中加入这样一行代码:console.log(VueRouter.prototype.push); 浏览器就把我们想要的呈现在控制台啦! 接下来让我们看看push到底是怎么实现的:

VueRouter.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;

  // $flow-disable-line
  if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
    return new Promise(function (resolve, reject) {
      this$1.history.push(location, resolve, reject);
    })
  } else {
    this.history.push(location, onComplete, onAbort);
  }
};

原来它的内部是调用了router实例的history的push方法,并且返回一个promise实例。没关系,我们继续查看new VueRouter().history. __proto __.push的源码:

  //从形参可以看出,push方法一共接收三个参数:第一个跳转路径,第二个成功的回调,第三个失败的回调。
  HashHistory.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;
    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(
      location,
      function (route) {
        pushHash(route.fullPath);
        handleScroll(this$1.router, route, fromRoute, false);
        onComplete && onComplete(route);
      },
      onAbort
    );
  };

好吧,它的内部调用的是transitionTo方法,我们再看看它的源码:

History.prototype.transitionTo = function transitionTo (location,onComplete,onAbort) {
  var this$1 = this;
  var route;
  // catch redirect option https://github.com/vuejs/vue-router/issues/3201
  try {
    route = this.router.match(location, this.current);
  } catch (e) {
    this.errorCbs.forEach(function (cb) {
      cb(e);
    });
    // Exception should still be thrown
    throw e
  }
  var prev = this.current;
  this.confirmTransition(
    route,
    function () {
      this$1.updateRoute(route);
      onComplete && onComplete(route);
      this$1.ensureURL();
      this$1.router.afterHooks.forEach(function (hook) {
        hook && hook(route, prev);
      });

      // fire ready cbs once
      if (!this$1.ready) {
        this$1.ready = true;
        this$1.readyCbs.forEach(function (cb) {
          cb(route);
        });
      }
    },
    function (err) {
      if (onAbort) {
        onAbort(err);
      }
      if (err && !this$1.ready) {
        // Initial redirection should not mark the history as ready yet
        // because it's triggered by the redirection instead
        // https://github.com/vuejs/vue-router/issues/3225
        // https://github.com/vuejs/vue-router/issues/3331
        if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
          this$1.ready = true;
          this$1.readyErrorCbs.forEach(function (cb) {
            cb(err);
          });
        }
      }
    }
  );
};

读完这个方法我们发现,它内部又调用了confirmTransition方法。但是到这里我们能发现这些函数的形参都是相似的:第一个参数是要去往的路由;第二个是成功的回调函数;第三个是出错的回调函数。可是回想我们使用pushreplace方法的时候,只传了一个参数,即要去往的路由。既然没有给出出错的回调函数,当出错时也就自然不会有函数来处理它。到这里我们就可以不再深入探究vuerouter的源码了。

原因解释和解决方案

我们回过头来捋一下对解决重复点击有用的信息:首先push方法内部使用了promise,其次push方法实际上有三个形参而我们平时只传了一个形参。 所以我们就想到了这么几个解决方案:

  • 在给push方法传参时补齐onAbort参数。这样push内部就可以利用传入的第三个回调函数帮我们处理异常了。(上面提到的方法三)
  • 既然push方法返回的是一个promise实例,我们就可以利用promise的异常穿透特性,给其加一个.catch,同样可以处理异常。(上面提到的方法二) 至于第一个方法(降低版本),并不推荐使用,因为新版本的报错实际上是为了优化性能(避免对当前位置的冗余导航),我们还是应该在新版本上进行优化,而不是开倒车。

后记:本文通篇使用push方法来举例,replace方法同理,故不再赘述。由于本人水平有限且时间仓促,如有错误或不当之处,烦请评论区指出,感激不尽!