前言:最近在做一个vue项目的时候遇到一个bug:重复点击导航组件中的某个按钮,控制台会报错。虽然并不会产生什么实际影响(目前看来是这样),但是打开控制台看着它就碍眼,所以在网上查了一下问题的原因,但是网上没有具体原因的阐述,只给出了解决方案。本文试图对其原因进行深度探究,想直接看解决方案的直接跳到解决办法。
bug具体情况
//template部分
<span " @click="goTo('/search')"></span>
//跳转方法
methods:{
goTo(path){
this.$router.replace(path)
}
}
当重复点击该span时,控制台会报一个error:
error分析
我们从错误信息可以看出该error的信息:Avoided redundant navigation to current location:(避免对当前位置的冗余导航),并且知道这个error跟promise有关。
从网上的解决方案开始探索
网上给出的解决方法主要有这个几个:
- 将vue router版本降低到3.0
- 在路由器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方法。但是到这里我们能发现这些函数的形参都是相似的:第一个参数是要去往的路由;第二个是成功的回调函数;第三个是出错的回调函数。可是回想我们使用push和replace方法的时候,只传了一个参数,即要去往的路由。既然没有给出出错的回调函数,当出错时也就自然不会有函数来处理它。到这里我们就可以不再深入探究vuerouter的源码了。
原因解释和解决方案
我们回过头来捋一下对解决重复点击有用的信息:首先push方法内部使用了promise,其次push方法实际上有三个形参而我们平时只传了一个形参。
所以我们就想到了这么几个解决方案:
- 在给push方法传参时补齐
onAbort参数。这样push内部就可以利用传入的第三个回调函数帮我们处理异常了。(上面提到的方法三) - 既然push方法返回的是一个promise实例,我们就可以利用promise的异常穿透特性,给其加一个
.catch,同样可以处理异常。(上面提到的方法二) 至于第一个方法(降低版本),并不推荐使用,因为新版本的报错实际上是为了优化性能(避免对当前位置的冗余导航),我们还是应该在新版本上进行优化,而不是开倒车。
后记:本文通篇使用push方法来举例,replace方法同理,故不再赘述。由于本人水平有限且时间仓促,如有错误或不当之处,烦请评论区指出,感激不尽!