[Vue.js进阶]从源码角度剖析vue-router(二)

前言

上篇中主要叙述了 vue-router 的注册和实例化过程,以及如何生成 $router, $route 对象html

在本篇中将会讲述:vue

  • $route 对象生成的时机html5

  • 路由守卫的原理git

  • 路由懒加载的原理github

文中的源码截图只保留核心逻辑 完整源码地址vue-router

有兴趣的朋友也能够看我学习源码时的详细注释源码地址数组

vue-router 版本:3.0.2promise

$route 对象生成的时机

在上篇中解释了在调用 new Router 生成 vue-router 实例时,会经过 createMatcher 给实例建立一个 matcher 对象,matcher 对象同时含有 matchaddRoutes 两个方法浏览器

图1:闭包

image

另外上篇中还讲了,在建立完 vue-router 实例后,调用 Vue.use(Router) 会混入2个全局钩子 beforeCreate 和 destroyed

图2:

image

此时图中第7行的 init 方法会初始化整个 vue-router ,而实例化和初始化 vue-router 是有区别的,实例化指的是经过 new Router 生成 vue-router 实例,初始化能够理解为进行全局第一次的路由跳转时,让 vue-router 实例和组件创建联系,使得路由可以接管组件

接下来咱们来看 vue-router 是如何初始化的

图3:

image

在上篇中我也讲述了 vue-router 会根据当前使用的路由模式(hash,html5,abstract)来生成 history 属性,接着 init 方法会根据 history 属性来执行不一样的逻辑,可是能够发现,不论是使用 hash 路由仍是 html5 的路由,都会执行 transitionTo 这个方法,它是整个路由跳转的核心方法

路由跳转

图4:(删除了取消路由导航的逻辑)

image

能够发现图4的第5行代码执行了 vue-router 实例的 match 方法,它最终会执行上篇咱们分析过的 matcher 属性的 match 方法,而且传入了2个参数

  • location:经过图3中的 getCurrentLocation 方法,最终会生成一个跳转目标的 loaction 对象(经过 push / replace 方法跳转),或一个跳转目标的路径(经过浏览器 url 跳转)

  • current:当前页面的路由 $route 对象

图5(示例):

image

图6:

image

继续沿用图5 的示例,当咱们直接在浏览器的 url 中输入http://localhost:8080/#/comp1/comp1Child 时,能够观察到 location 参数为跳转目标的路径,而且此时是全局第一次调用 transitionTo 方法,vue-router 默认第一次跳转的 current 参数为根路径的 $route 对象,而之后的跳转,current 会变成当前路由的 $route 对象

图7(第一次 history.current 值为根路径转换而来的 $route 对象):

image

分析过 match 方法的2个参数后,接着会执行上篇中分析过的 match 方法

(在建立 $router 的 match 方法中,其实 current 参数通常不多用到,主要围绕 location 参数再结合3个路由映射表生成 $route 对象)

图8(执行图4的 vue-router 实例的 match 方法最后会执行到上篇分析的 match 方法):

image

此时,这个最终执行的这个 match 方法就会建立出一个 $route 对象,并赋值给图4的 route 属性,随后会进入 confirmTransition 这个方法,它负责控制全部的路由守卫的执行,咱们来看一下它的内部是如何运行的

路由守卫的原理

本小结会介绍 vue-router 一个比较重要的部分:路由守卫

和组件的生命周期的钩子不一样,路由守卫将重点放在路由上,可以控制路由跳转,通常用在页面级别的路由跳转时控制跳转的逻辑,好比在路由守卫中检查用户是否有进入当前页面的权限,没有则跳转到受权页面,亦或是在离开页面时警告用户有未确认的信息,确认后才能跳转等等

在路由守卫中,通常会接收3个参数,to,from,next,前两个分别是跳转后和跳转前页面路由的 $route 对象,第三个参数 next 是一个函数,当执行 next 函数后会进行跳转,若是一个包含 next 参数的路由守卫里没有执行该函数,页面会没法跳转,接下来咱们来解密路由守卫背后的原理

寻找跳转先后路由的区别

图9:

image

首先会拿到当前的页面的 $route 对象,这个在刚刚分析过,接下来会执行 resolveQueue 函数,这个函数的做用是根据跳转前和跳转后 $route 对象的 matched 数组,返回这2个数组包含的路由记录的区别

在上篇中提到, $route 对象的 matched 属性是一个数组,经过 formatMatch 函数最终返回 $route 对象以及全部父级的路由记录

resolveQueue 返回3个数组,updated 表明跳转先后 matched 数组相同部分,deactivated 表明删除部分,activated 表明新增部分,举个例子,当咱们从 comp1Child 页面跳转到 comp2 页面,这3个数组分别对应的值

图10:

image

图11:

image

跳转时哪些组件触发哪些路由守卫就是由这3个数组决定的,从这里就能够大体推断出,vue-router 会在新增的组件会触发 beforeRouteEnter 之类的进入守卫,在相同部分触发 beforeRouteUpdate 守卫,在删除部分触发 beforeRouteLeave 之类的离开守卫

生成路由守卫

接下来咱们来证实上述的推断,执行到图9的第 9 行会声明一个 queue 数组, vue-router 会将这些相同的不一样的路由记录通过一些函数的转换,最后放到该数组中,而且经过旁边定义的类型可以发现,数组的元素都是 NavigationGuard 类型

图12:

image

能够发现 NavigationGuard 就是一个标准的路由守卫的签名,能够推断出,通过 queue 数组内部这些函数的转换最终会返回路由守卫组成的数组,而这些函数就是将上节中的路由记录转换为路由守卫的函数

同时数组中的守卫的排列顺序也是设计好的,对应 vue-router 官方文档中提到的路由导航解析流程

图13:

image

咱们先分析 queue 数组里第一个执行的函数 extractLeaveGuards,通过一层封装最终会执行通用函数 extractGuards

图14:

image

此时 records 参数为删除的路由记录,name 为 beforeRouteLeave,即最终触发的是 beforeRouteLeave 守卫

图15:

而后会执行 flatMapComponents 函数,这个函数也是一个通用函数,做用是遍历 records 数组,每次执行第二个回调函数,相似数组的 map 方法,而对应图14中回调函数参数解释以下

  • def:视图名对应的组件配置项(由于 vue-router 支持命名视图因此可能会有多个视图名,大部分状况为 default,及使用默认视图),当是异步路由时,def为异步返回路由的函数

  • instance:组件实例

  • match:当前遍历到的路由记录

  • key:视图名

在回调函数内部会执行 extractGuard 函数

图16:

image

def 为组件配置项,经过 Vue 核心库的函数 extend 将配置项转为组件构造器(虽然配置项中就能拿到对应的路由守卫,可是从官方注释发现只有转为构造器后才能拿到一些全局混入的钩子),在生成构造器时,Vue 会将配置项赋值给构造器的静态属性 options(extend 部分的解析能够看我另外一篇博客),最后返回配置项中对应的路由守卫函数,即若是咱们在跳转后的组件中定义了 beforeRouteLeave 的话这里就会返回这个函数

在图 14 中拿到返回值 guard 后会通过一层处理,例如扁平化,绑定 this 指向,根据 reverse 参数决定是否要反转数组(由于 matched 中路由记录顺序是父 => 子,而 beforeRouteLeave 须要从最里层子组件触发,因此须要进行反转保证守卫触发顺序),最后 queue 数组的元素以下

图17:

image

值得注意的是最后一个 resolveAsyncComponents 函数,它的做用是解析异步路由

路由懒加载的原理

什么是异步路由呢,通俗来讲就是使用路由懒加载返回的路由,咱们可使用 import () 这种语法去动态的加载 JS 文件,放到 vue-router 中,就能够实现异步加载组件配置项(这里只讨论开发中使用较多的 import() 语法)

图18:

image

咱们进入函数内部一探究竟

图19:

image

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 不就行了吗

接下来咱们进入函数内部一探究竟

遍历 queue 数组

图20:

image

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行代码:

image

其中迭代器的参数 next 即 runQueue 中的 step 函数

咱们知道,当在路由守卫中若是没有执行 next 函数,路由将没法跳转,缘由是由于没有去执行 hook 的第三个回调函数,也就不会执行 iterator 的第三个参数 next,最终致使不会通知 runQueue 继续往下遍历

另外当咱们给 next 函数传入另外一个路径时,会取消原来的导航,取而代之跳转到指定的路径,缘由是由于知足上图的 true 逻辑,执行 abort 函数取消导航,随后会调用 push/replace 将路由从新跳转到指定的页面

最后回到以前异步路由中提到的那个 next 函数,当全部的异步路由都被解析完成后,才会执行 next 函数继续遍历 queue 数组的下个元素,一旦有某个路由没有被解析完成,vue-router 就会一直等待直到接受到为止,而后才会去触发以后的逻辑

遍历成功后的回调

当 queue 最后一个元素也就是异步组件被解析完成后,runQueue 会执行传入的第三个参数,即执行遍历成功回调

对应图 9 中的 44-64 行:

image

能够看到成功回调里 vue-router 又往 queue 中添加了路由守卫,同时会开启第二轮遍历......

image

关于第二轮的 queue 数组遍历碍于篇幅我会放到下篇来讲

总结

  • 当 vue 的根实例被实例化时,会执行 vue-router 的初始化逻辑,和 vue-router 的实例化不一样的是,它的初始化在实例化以后,做用是创建 vue-router 和 Vue 组件之间的关系

  • 当初始化时会进行第一次路由跳转,根据跳转路径生成 loaction 对象,再经过 location 对象生成 $route

  • $route 对象的 matched 属性保存了当前和全部父级的路由记录,在路由跳转时会根据跳转先后 $route 对象的这2个 matched 属性,区分出相同和不一样的路由记录,来决定哪些组件触发哪些路由守卫

  • vue-router 经过回调的形式异步的执行路由守卫,当前一个解析完毕后会调用回调继续执行下个守卫

  • 只有懒加载的路由都加载完成后,才会执行上述的回调,继续执行下个守卫,不然会一直等待

参考资料

Vue.js 技术揭秘