前言
在上篇中主要叙述了 vue-router 的注册和实例化过程,以及如何生成 $router, $route 对象
在本篇中会讲述:
$route 对象生成的时机
路由守卫的原理
路由懒加载的原理
文中的源码截图只保留核心逻辑 完整源码地址
vue-router 版本:3.0.2
$route 对象生成的时机
在上篇中解释了在调用 new Router 生成 vue-router 实例时,实例会包含一个 matcher 对象,它是通过 createMatcher
创建的,matcher 对象含有 match
和 addRoutes
两个方法
图1:
另外上篇中还讲了,在创建完 vue-router 实例后,调用 Vue.use(Router) 会混入2个全局钩子 beforeCreate 和 destroyed
图2:
此时图中第7行的 init
方法会初始化整个 vue-router ,而实例化和初始化 vue-router 是有一点的区别的,实例化指的是通过 new Router 生成 vue-router 实例,初始化可以理解为进行全局第一次的路由跳转时,让 vue-router 实例和组件建立联系,使得路由能够接管组件
接下来我们进入到 vue-router 的 init
方法
图3:
在上篇中我讲述了 vue-router 实例的 history 属性等于当前使用的路由模式(hash,html5,abstract)的实例,init
方法会根据 history 属性也就是根据不同模式的路由来执行不同的逻辑,但是可以发现,不管是使用 hash 路由还是 html5 的路由,都会执行 transitionTo
这个方法,它是整个路由跳转的核心方法
路由跳转
图4:(删除了取消路由导航的部分逻辑,这里只分析跳转成功的逻辑)
可以发现图4的第5行代码执行了 vue-router 实例的 match 方法,它最终会执行上篇我们分析过的 matcher 属性的 match
方法,并且传入了2个参数
location:通过图3中的
getCurrentLocation
方法,最终会生成一个跳转目标的 loaction 对象(通过 push / replace 方法跳转),或一个跳转目标的路径(通过浏览器 url 跳转)current:当前页面的路由 $route 对象
图6:
继续沿用上篇中示例,当我们直接在浏览器的 url 中输入http://localhost:8080/#/comp1/comp1Child
时,可以观察到 location 参数为跳转目标的路径,并且此时是全局第一次调用 transitionTo
方法,vue-router 默认第一次跳转的 current 参数为根路径的 $route 对象,而以后的跳转,current 会变成当前路由的 $route 对象
图7(第一次 history.current 值为根路径的 $route 对象):
分析过 match
方法的2个参数后,接着会执行上篇中分析过的 match
方法
(在创建 $router 的 match
方法中,其实 current 参数一般很少用到,主要围绕 location 参数再结合3个路由映射表生成 $route 对象)
图8(执行图4的 vue-router 实例的 match 方法最后会执行到上篇分析的 match
方法):
此时,这个最终执行的这个 match
方法就会创建出一个 $route 对象,并赋值给图4的 route 属性,随后会进入 confirmTransition
这个方法,它负责控制所有的路由守卫的执行,我们来看一下它的内部是如何运行的
路由守卫的原理
本小结会介绍 vue-router 一个比较重要的部分:路由守卫
和组件的生命周期的钩子不同,路由守卫将重点放在路由上,能够控制路由跳转,一般用在页面级别的路由跳转时控制跳转的逻辑,比如在路由守卫中检查用户是否有进入当前页面的权限,没有则跳转到授权页面,亦或是在离开页面时警告用户有未确认的信息,确认后才能跳转等等
在路由守卫中,一般会接收3个参数,to,from,next,前两个分别是跳转后和跳转前页面路由的 $route 对象,第三个参数 next 是一个函数,当执行 next 函数后会进行跳转,如果一个包含 next 参数的路由守卫里没有执行该函数,页面会无法跳转,接下来我们来解密路由守卫背后的原理
寻找跳转前后路由的区别
图9:
首先会拿到当前的页面的 route 对象的 matched 数组,返回这2个数组包含的路由记录的区别**
在上篇中提到, $route 对象的 matched 属性是一个数组,通过 formatMatch
函数最终返回 $route 对象以及所有父级的路由记录
resolveQueue
返回3个数组,updated 代表跳转前后 matched 数组相同部分,deactivated 代表删除部分,activated 代表新增部分,举个例子,当我们从 comp1Child 页面跳转到 comp2 页面,这3个数组分别对应的值
图10:
图11:
跳转时哪些组件触发哪些路由守卫就是由这3个数组决定的,从这里就可以大致推断出,vue-router 会在新增的组件会触发 beforeRouteEnter 之类的进入守卫,在相同部分触发 beforeRouteUpdate 守卫,在删除部分触发 beforeRouteLeave 之类的离开守卫
生成路由守卫
接下来我们来证明上述的推断,执行到图9的第 9 行会声明一个 queue 数组,它是一个队列,看到 vue-router 会将这些相同的不同的路由记录经过一些函数的转换,最后放到数组中
通过旁边定义的类型能够发现,数组的元素都是 NavigationGuard 类型
图12:
可以发现 NavigationGuard 就是一个标准的路由守卫的签名,可以推断出,经过 queue 数组内部这些函数的转换最终会返回路由守卫组成的数组,而这些函数就是将上节中的路由记录转换为路由守卫的函数
同时数组中的守卫的排列顺序也是设计好的,对应 vue-router 官方文档中提到的路由导航解析流程
图13:
我们先分析 queue 数组里第一个执行的函数 extractLeaveGuards
,经过一层封装最终会执行通用函数 extractGuards
图14:
此时 records 参数为删除的路由记录,name 为 beforeRouteLeave,即最终触发的是 beforeRouteLeave 守卫
然后会执行 flatMapComponents
函数,这个函数也是一个通用函数,作用是 records 数组,每次执行第二个回调函数,类似数组的 forEach 方法,而回调的参数解析如下
def:视图名对应的组件配置项(因为 vue-router 支持命名视图所以可能会有多个视图名,大部分情况为 default,及使用默认视图),当是异步路由时,def为异步返回路由的函数
instance:组件实例
match:当前遍历到的路由记录
key:视图名
在回调函数内部会执行 extractGuard
函数
图15:
def 为组件配置项,通过 Vue 核心库的函数 extend 将配置项转为组件构造器(虽然配置项中就能拿到对应的路由守卫,但是从官方注释发现只有转为构造器后才能拿到一些全局混入的钩子),在生成构造器时,Vue 会将配置项赋值给构造器的静态属性 options(extend 部分的解析可以看我另一篇博客),最后返回配置项中对应的路由守卫函数,即如果我们在跳转后的组件中定义了 beforeRouteLeave 的话这里就会返回这个函数
在图 14 中拿到返回值 guard 后会经过一层处理,例如扁平化,绑定 this 指向,根据 reverse 参数决定是否要反转数组(因为 matched 中路由记录顺序是父 => 子,而 beforeRouteLeave 需要从最里层子组件触发,所以需要进行反转保证守卫触发顺序),最后 queue 数组的元素如下
图16:
值得注意的是最后一个 resolveAsyncComponents
函数,它的作用是解析异步路由
路由懒加载的原理
什么是异步路由呢,通俗来说就是使用路由懒加载返回的路由,我们可以使用 import ()
这种语法去动态的加载 JS 文件,放到 vue-router 中,就可以实现异步加载组件配置项即路由懒加载(这里只讨论开发中使用较多的 import()
语法)
图17:
我们进入函数内部一探究竟
图18:
resolveAsyncComponents
函数最终会返回一个函数,并且符合路由守卫的函数签名(这里 vue-router 可能只是为了保证返回函数的一致性,实质上在这个函数中,并不会用到 to,from 这2个参数)
这个函数只是被定义了,并没有执行,但是我们可以通过函数体观察它是如何加载异步路由的。同样通过 flatMapComponents
遍历新增的路由记录,每次遍历都执行第二个回调函数
在回调函数里,会定义一个 resolve
函数,当异步组件加载完成后,会通过 then 的形式解析 promise,最终会调用 resolve
函数并传入异步组件的配置项作为参数, resolve
函数接收到组件配置项后会像 Vue 中一样将配置项转为构造器 ,同时将值赋值给当前路由记录的 componts 属性中(key 属性默认为 default)
另外 resolveAsyncComponents
函数会通过闭包保存一个 pending 变量,代表接收的异步组件数量,在 flatMapComponents
遍历的过程中,每次会将 pending 加一,而当异步组件被解析完毕后再将 pending 减一,也就是说,当 pengding 为 0 时,代表异步组件全部解析完成, 随即执行 next
方法,next
方法是 vue-router 控制整个路由导航顺序的核心方法
执行路由守卫
在分析 next
方法之前,我们先来看一下 vue-router 是如何处理 queue 数组中的元素的,在上文中,虽然定义了 queue 数组,其中包括了路由守卫以及解析异步组件的函数,但是还没有执行
走到图 9 的 24 行,定义了一个 iterator
函数,顾名思义它是一个迭代器,最后将 queue 和这个迭代器放入 runQueue
函数执行,由此可以发现这个 runQueue
是一个用来遍历 queue 数组的函数,看到这里有些朋友会有疑问,为啥 vue-router 大费周章的定义一个 runQueue 函数,直接一个 forEach 不就好了吗
[图片上传失败...(image-554cb-1558881177066)]
接下来我们进入函数内部一探究竟
遍历 queue 数组
图19:
runQueue
内部声明了一个 step
的函数,它一个是控制 runQueue
是否继续遍历的函数,当我们第一次执行时,给 step
函数传入参数 0 表示开始遍历 queue 第 1 个元素,通过 step
函数内部可以发现,它最终会执行参数 fn,也就是 iterator
这个迭代器函数,给它传入当前遍历的 queue 元素以及一个回调函数,这个回调函数里保存着遍历下个元素的逻辑,也就是说runQueue
将是否需要继续遍历的控制权传入了 iterator
函数中
这里先抛出结论
runQueue
函数只负责遍历数组,并不会执行逻辑,它依次遍历 queue 数组的元素,每次遍历时会将当前元素交给外部定义的iterator
迭代器去执行,而iterator
迭代器一旦处理完元素就让runQueue
再次执行下个元素,当数组全部遍历结束时,会执行参数 cb 这个回调函数
runQueue
和普通的 forEach 遍历数组不同点在于,forEach 是同步的,而 vue-router 中可能会存在异步路由,所以需要设计一个支持异步的遍历函数,只有当 iterator
函数执行完一次后 runQueue
才会接着遍历下一个元素
接着我们来看一下 runQueue
将元素交给迭代器执行时发生了什么
迭代器
对应图 9 中24-41行代码:
其中迭代器的参数 next 即 runQueue
中的 step
函数
我们知道,当在路由守卫中如果没有执行 next
函数,路由将无法跳转,原因是因为没有去执行 hook
的第三个回调函数,也就不会执行 iterator
的第三个参数 next
,最终导致不会通知 runQueue
继续往下遍历
另外当我们给 next
函数传入另一个路径时,会取消原来的导航,取而代之跳转到指定的路径,原因是因为满足上图的 true 逻辑,执行 abort
函数取消导航,随后会调用 push/replace 将路由重新跳转到指定的页面
最后回到之前异步路由中提到的那个 next
函数,当所有的异步路由都被解析完成后,才会执行 next
函数继续遍历 queue 数组的下个元素,一旦有某个路由没有被解析完成,vue-router 就会一直等待直到接受到为止,然后才会去触发之后的逻辑
遍历成功后的回调
当 queue 最后一个元素也就是异步组件被解析完成后,runQueue
会执行传入的第三个参数,即执行遍历成功回调
对应图 9 中的 44-64 行:
可以看到成功回调里 vue-router 又往 queue 中添加了路由守卫,同时会开启第二轮遍历......
关于第二轮的 queue 数组遍历碍于篇幅我会放到下篇来说
总结
当 vue 的根实例被实例化时,会执行 vue-router 的初始化逻辑,和实例化不同的是,初始化在实例化之后,作用是建立 vue-router 和 Vue 组件之间的关系
当初始化时会进行第一次路由跳转,根据跳转路径生成 loaction 对象,再通过 location 对象生成 $route
$route 对象的 matched 属性保存了当前和所有父级的路由记录,在路由跳转时会根据跳转前后 $route 对象的这2个 matched 属性,区分出相同和不同的路由记录,来决定哪些组件触发哪些路由守卫
vue-router 通过回调的形式异步的执行路由守卫,当前一个解析完毕后会调用回调继续执行下个守卫
只有懒加载的路由都加载完成后,才会执行上述的回调,继续执行下个守卫,否则会一直等待