阅读 976

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

前言

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

在本篇中将会讲述:

  • $route 对象生成的时机

  • 路由守卫的原理

  • 路由懒加载的原理

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

有兴趣的朋友也可以看我学习源码时的详细注释源码地址

vue-router 版本:3.0.2

$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 技术揭秘

关注下面的标签,发现更多相似文章
评论