浅显易懂的vue-router源码解析(⼆)
基础回顾
上篇⽂章已经详细介绍了vue-router的整体运作流程,想了解上篇⽂章具体内容可以.上⼀篇留下了导航守卫(俗称钩⼦)没有展开分析.本篇⽂章将着重研究导航守卫的实现原理.
在学习源码之前,我们先对导航守卫的API做⼀个基本回顾,源码做的⼤部分事情就是为了实现这些API.
以全局前置守卫router.beforeEach为案例讲解其⽤法,其他导航守卫依次类推.
全局前置守卫是在实际中使⽤⾮常多的API.每⼀次页⾯的跳转都会执⾏router.beforeEach包裹的函数(代码如下).
to是将要进⼊的⽬标路由对象,from是当前导航正要离开的路由.
next函数作⽤⾮常强⼤.它可以决定是放⾏到下⼀级守卫还是中间截断直接跳转到其他页⾯.
next(false)或者next(error)表⽰中断当前的导航.
next({ path: '/' }) 或者 next({ path: '/', replace: true })表⽰跳转或重定向到path路径.
next()表⽰当前导航守卫放⾏,进⼊下⼀个钩⼦.
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
next({ path: '/login', replace: true }); //重定向到登录页⾯
})
其他主要的导航守卫参数也由to、from和next组成,⽤法和router.beforeEach⼀样,只是执⾏的时机不同.
beforeRouteLeave:导航即将离开某个页⾯组件时,组件内定义的beforeRouteLeave钩⼦会触发.
beforeRouteUpdate:对于⼀个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的 Foo 组件,因此Foo 组件内定义的beforeRouteUpdate钩⼦会触发.
beforeEnter:在开发者编写的路由配置⾥⾯添加的钩⼦函数.它会在进⼊某页⾯组件之前执⾏.
异步路由组件: 异步加载组件 const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue').
beforeRouteEnter:它会在进⼊某页⾯组件之前执⾏,与beforeEnter守卫相⽐,除了执⾏时机不同,它还定义在组件内.
这⼏个导航守卫的执⾏顺序依次如下.上⼀个导航守卫函数内调⽤next()便可触发下⼀个导航守卫继续执⾏.
beforeRouteLeave  // 在失活的组件⾥调⽤
beforeEach  // 全局定义
beforeRouteUpdate  // 在重⽤的组件⾥调⽤
beforeEnter // 在路由配置⾥调⽤
解析异步路由组件
beforeRouteEnter // 在被激活的组件⾥调⽤
调⽤场景
按照上⼀篇⽂章所讲,vue-router最终执⾏的跳转都是调⽤下⾯transitionTo函数.
location是跳转路径,onComplete和onAbort分别代表跳转成功或失败的回调函数.
⾏firmTransition第⼆个参数,象征着导航成功的回调函数.
ansitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;
var route = uter.match(location, this.current);
route,
function () {
this$1.updateRoute(route);
onComplete && onComplete(route);
...
},
function (err) {
if (onAbort) {
onAbort(err);
}
...
}
);
};
我们可以先试着猜想⼀下firmTransition是如何对待访问路由进⾏层层拦截的?
导航守卫的基本概念已经知晓,它是开发者⾃⼰定义的拦截函数.现在假设我们已经将所有定义的导航守卫全部收集起来存到了queue数组.那我们怎么去实现那种在导航守卫⾥运⾏next()就能跳到下⼀个导航钩⼦呢?
next实现
全局前置守卫router.beforeEach在上⾯已经介绍过,我们这次主要想要研究⼀下函数⾥⾯的next是如何实现的(代码如下).
next()函数如果什么都不传表⽰放⾏直接进⼊下⼀个导航钩⼦.
如果next⾥⾯填⼊的是⼀个对象,对象⾥⾯只包含path属性时,导航会push到该路径.对象要是除了path外,还存在replace:true,那么导航会重定向到path路径.
最后next(false)传递的参数是⼀个false或者Error实例时,导航就会终⽌本次跳转操作,接下来的钩⼦链条也会停⽌往下执⾏.
router.beforeEach((to, from, next) => {
if (!user_info) { //没有登录跳到登录页⾯
next({  path:"/login" });
} else {
//登录过了直接放⾏
next();
}
})
从上⾯对next()携带的参数分析,next内部会对不同参数类型做不同的处理,我们看下源码是如何处理参数的(代码如下).
当next执⾏后,下⾯的匿名函数就会被调⽤.to分别对应了上述的三种情况.如果to为false或者Error类型终⽌跳转.如果to是⼀个object对象或者字符串,就使⽤push或replace执⾏跳转.如果to为空直接执⾏next放⾏.
function (to) {
if (to === false || isError(to)) {
typeof的用法
abort(to);  // 终⽌本次跳转操作
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
abort();
if (typeof to === 'object' && to.replace) {
place(to);
} else {
this$1.push(to);
}
} else {
// confirm transition and pass on the value
next(to);
}
我们可以看到在这个匿名函数⾥⾯,最后⼀个else⾥也包含⼀个next,这个next⼀调⽤才是真正的触发下⼀个导航钩⼦.
现在的问题是应该设计⼀个什么样的机制才能满⾜这样的⼀种链式调⽤.⾸先我们要先把所有开发者定义的导航钩⼦先收集起来,按照执⾏顺序的先后存储在⼀个数组⾥⾯.⽐如queue = [fn1,fn2,fn3,fn4].假设fn1对应着beforeRouteLeave钩⼦,fn2对应着beforeEach钩⼦.
然后设置起始索引index = 0,让数组按照索引取出函数执⾏.那在fn1⾥⾯为什么调⽤next就可以触发fn2执⾏呢?那时因为当index = 0执
⾏fn1时,queue [index+1]的执⾏机制被包裹成函数参数传⼊到了fn1⾥⾯来控制.因此fn1内部调⽤next函数就会触发fn2函数的执⾏.源码实现如下.
function runQueue (queue, fn, cb) {
var step = function (index) {
if (index >= queue.length) {
cb();
} else {
if (queue[index]) {
fn(queue[index], function () {
step(index + 1);
});
} else {
step(index + 1);
}
}
};
step(0);
}
queue存储着所有导航钩⼦函数,第⼀次执⾏取出queue的第⼀个函数放⼊fn中执⾏,在fn的第⼆个参数放置了⼀个匿名函数,这个匿名函数正是导航守卫⾥⾯调⽤next()最终能触发下⼀个导航钩⼦执⾏的原因.
我们接下来看下fn的源码,fn就是下⾯的iterator函数.
hook参数对应着从上⾯queue中取出的钩⼦函数,在调⽤hook时传⼊了三个参数.route是下⼀个路由对象,current为当前路由对象,⽽第三个函数正是我们在上⾯介绍的对to参数处理的匿名函数.
假设hook对应着全局前置守卫 router.beforeEach((to, from, next) => { ... },那么路由守卫的三个参数to,from和next就分别由下⾯
的route,current和匿名函数传⼊.
当router.beforeEach的next执⾏,hook第三个参数匿名函数就会执⾏.匿名函数根据to的数据类型做出相应的判断,当to为空时,匿名函数执⾏了iterator函数的第⼆个参数next.
⽽这个next是runQueue⾥⾯的function () { step(index + 1) }作为参数传进来的.最终就会触发下⼀个导航钩⼦的执⾏.
var iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort()
}
try {
hook(route, current, function (to) {
if (to === false || isError(to)) {
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
abort();
if (typeof to === 'object' && to.replace) {
place(to);
} else {
this$1.push(to);
}
} else {
next(to);
}
});
} catch (e) {
abort(e);
}
};
整体流程
现在我们来回顾⼀下整个执⾏流程.导航执⾏跳转时调⽤transitionTo(location),然后uter.match将跳转路径location转化成待跳转的路由对象route.
ansitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var route = uter.match(location, this.current);
route,
function () {
//成功的回调函数
},
function (err) {
/
/失败回调
}
);
};
route是执⾏firmTransition传递进来的参数,current是从History实例中获取的,存储着当前的路由对象.
firmTransition = function confirmTransition (route, onComplete, onAbort) {
var current = this.current;
...
}
应⽤初始化时current被赋予了⼀个固定值(如下),path指向了根路径.只有当导航成功调⽤上⾯onComplete函数时,才会将待跳转的路由对
象route赋值给当前路由对象current.
current = {
fullPath: "/",
hash: "",
matched: [],
meta: {},
name: null,
params: {},
path: "/",
query: {}
}
在回调函数⾥⾯⼜执⾏了⼀次runQueue,这次是将beforeRouteEnter和全局定义的beforeResolve收集起来赋值给queue,再执⾏⼀遍上述的流程.等到queue⾥⾯的导航钩⼦执⾏完毕后,执⾏成功的回调函数onComplete.⾛到这⼀步表⽰所有导航守卫全部通过,该路径允许跳转.
firmTransition = function confirmTransition (route, onComplete, onAbort) {
var current = this.current;
var queue = ... // 收集导航钩⼦
var iterator = function (hook, next) {
...
}
runQueue(queue, iterator, function () {
// 成功的回调函数,执⾏到这⾥说明 queue ⾥⾯的所有导航钩⼦都通过了
var queue = ...; //收集组件内部的 beforeRouteEnter 钩⼦函数和全局的  beforeResolve 钩⼦
runQueue(queue, iterator, function () {
...
onComplete(route);
...
});
}
导航守卫收集
上⾯已经将导航守卫的执⾏逻辑梳理了⼀遍,但怎么去收集queue并没有展开,接下来深⼊研究⼀下queue的收集过程.
我们继续看firmTransition源码.resolveQueue函数传⼊了当前路径对象current和待跳转路由对象route的matched属性.
上⼀篇⽂章已经详细介绍过,matched的数据结构形似[{path:"/login",meta:{},name:"login",components:组件对象 }],通过matched可以拿到页⾯组件的参数.

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。