聊聊 vue-router

echo  |  2019. 05. 30   |  阅读 217 次
frontend

之前要做一个路由按需加载的需求,所以参考了 vue-router 的实现,进而更加深入的了解了 vue-router

看源码最好的就是打断点来看。将 vue-router 代码 clone 下来,可以看到他有一个 examples 目录,这里就是 vue-router 的一些使用 demo ,同时看下 package.json 文件,可以看到 scripts 中有一个 dev 命令,跑的就是上述的各个 demo。现在我们来跑下这个命令,可以看见如下的界面:

image

注册VueRouter

首先我们直接看嵌套路由的使用,找到examples下的 nested-routes 。其路由配置项如下:

carbon (3)

作为一个插件,我们需要使用 Vue.use 来注册下该插件 :Vue.use(VueRouter),使得在vue中能使用 vue-router,这一步会调用 vue-router 中的 install 函数。来看下 vue-router 的注册函数。重点看下下面这段:

carbon (5)

所以是在vue中的 beforeCreate 生命周期中注入的 vueRouter ,首先会将 router配置 传入根 Vue 中的 _router 变量,同时初始化 VueRoute r实例,并 监听 _router 变量的变化,从而重新触发组件的渲染。如果没有 router 这个配置,就向上找到 _routerRoot。 然后在 Vue的原型链 上使注册 $router/$route ,实例在获取值的时候,就能分别获取到 VueRouter的实例 以及 当前路由 相关的信息,在组件中我们就能像 this.$route.params.usernamethis.$router.go(-1) 一样使用它们。 接着就是注册 View/Link 这两个组件了。最后是合并策略的一些设置。

实例化 Vuerouter

注册完之后,我们就要开始实例化 VueRouter ,并传入构建化参数。 现在我们现在 install 中的 beforeCreate 和 VueRouter 构造函数中打个断点。

image

createMatcher

该函数是用于创建路由匹配对象的。下面是它的最终返回值:

image

现在进入其内部看看具体做了啥。它接收两个参数,一个是具体路由配置项,一个是实例本身。首先它会调用 createRouteMap 来一些路由信息的收集,返回的的对象中包含如下三个属性:

  • pathList:配置列表中的路由路径集合
  • pathMap:路径所对应的具体路由信息的映射
  • nameMap:具名路由的具体路由信息的映射

basic demo中的配置最终生成的三个变量结果如下:

createRouteMap 会调用 addRouteRecord 来迭代配置项中的每一个节点,并将它们记录在 pathList/pathMap/nameMap 这三个变量中。然后会将通配符挪到 pathList的最后面 。以便只有在前面路由不匹配的情况下在来匹配默认路由。 addRouteRecord 会生成一个路由对象,流程如下:

image

这里有一点关于 pathList 中里面元素的顺序想记录下,如下图,下面是上面 demo 的路由树状图:

image

pathList 会先记录最底层的 叶子节点的path(由父节点的path和自己的path组合而成),然后在一层层向上记录父级的 path,这里会和后面说到的具体匹配的时候想关联。然后如果有 alias ,则继续去其子节点做一次匹配,同时做好信息记录。上面我给 parent 增加一个 alias: ['p']。所以最终的 pathList 如下:

image

敲黑板了:一开始在 normalizePath 函数中对于以 / 开头的 path 就直接返回存在一些疑问,后来看到官方曾提到: 以 / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。然后想了想,如果我想 /baz 开头的路由与 /parent 开头的路由 共用一部分嵌套组件 ,如这里的 Parent 组件,如果没有上述的设置,那么我需要重新书写一个 /parent 路由配置,只不过把 parent 改成 baz,那么久有点多余了,所以直接向上面一样,就能实现重复利用了,这样,也就可以理解上面对于 alias 的设置为何对于 /baz 不管用了,因为这只是一个快捷操作啊,alias针对的是 parent,而不是 baz

讲完 matcher ,下面就是根据用户定义以及浏览器的支持情况来选择不同的路由策略。我们知道,在浏览器端路由方案有 hash 以及 html5 支持的 historyAPI,两种方案,分别监听相对应的事件来渲染不同的组件。

carbon

先来看 html5 的 history 模式,在 VueRouter 中,不管是 history,还是 hash 以及非浏览器端的 abstract 模式,都是继承与一个 History 类,而三种模式都会将 vueRouter 实例以及构造参数中的base选项传到对应的构造函数中,这里为 /nested-routes,最终传入 History 这个基类中。

初始化

前面讲到,在注册 vue-router 的时候会调用实例的 init 方法来完成初始化。 init 中会先进行是否已注册 VueRouter,是否初始化,收集 vue 实例的准备工作。然后根据之前的判断来决定调用哪种路由模式,这里为 history 模式。然后会调用其 transitionTo 方法,该方法继承于 History 类,传入当前的 location 信息,由 getCurrentLocation 获得。

carbon (1)

transitionTo/confirmTransition

transitionTo 还有两个参数用于匹配完成后的回调以及匹配过程中中断的回调。该函数首先会调用由createMatcher 返回的 match 方法。这里先讲解下该类中的 current 变量,用于保存 当前的路由 。在初始化基类的时候,会先赋一个初始值 —— START,由 createRoute 方法生成。

image

回到 match 函数,

carbon (2)

回到 transitionTo 函数,找到真正匹配的路由对象之后,就调用 confirmTransition 来判断是否真的要跳转到该路由,主要是一个路由钩子以及异步路由组件的处理。 首先它会判断要跳转到的路由是否与当前路由相匹配,如果是,就终止跳转。

总的流程如下:

image

如何处理路由钩子以及异步组件

我们来重点讲下内部是如何处理钩子以及异步组件的。首先来讲下是如何获取到各个钩子的。 这个功能主要依赖于 extractGuards 这个方法,具体代码如下:

carbon (2)

如何执行这些钩子呢?VueRouter 封装了一个 runQueue 函数来执行他们 :

carbon (3)

简单的几行就实现了同步执行异步函数的效果。 那么对于每个钩子我们要做什么呢?来看下迭代函数 iterator

carbon

这样当上一个执行完之后才会执行队列中的下一个元素。现在来看下异步组件是如何处理的,VueRouter 定义了一个 resolveAsyncComponents 函数,专门用于处理异步组件,具体代码参见如下:

carbon (1)

当 queue 执行完之后,就调用回调函数,处理 beforeRouteEnterresolveHooks 钩子,继续调用 runQueue,当这个 queue 执行完之后,就调用 onComplete 函数,更新 当前路由 current,执行由 listen 函数得到的 监听函数,并且执行全局的 afterEach 钩子,接着将 ready 置为 true ,最后执行 readyCbs 队列中的回调函数,相关代码参见 transitionTo

至此 transitionTo 就执行完毕了,回到 VueRouter 类的 init 方法,后面即是增加监听函数,只有在路由改变的时候执行。而后就是监听 onpopstate 或者 onhashchange 事件来重新进行渲染。

之前我们在写 beforeRouteEnter 这个钩子函数的时候,是获取不到组件实例的,因为在组件渲染之前就被执行了,但是我们可以传入一个函数,参数为组件实例,这样就能获取到组件的实例了,这是怎么做到的呢? 首先在收集 beforeRouteEnter 钩子的时候回绑定一个函数,该函数会在 next(cb=>{}) 的时候收集这个函数,代码如下:

carbon (2)

从上述代码中可看见,这个收集的函数里面会有一个轮询,一直到该组件实例生成之后,才会执行回调函数,从而能获取到组件实例,而上述说的 cbs 则会在组件的下一个事件循环中被执行,由 $nextTick 实现。

vueRouter 的滚动行为

首先,在每个 mode 构造函数被执行的时候,回先执行 setupScroll 函数。而后在监听 popstate/hashchange 变化的时候,在路由跳转成功的时候再次执行与滚动相关的 handleScroll 函数。总的来说,就是在路由确定跳转的时候,即在执行 onCompelete 函数的时候,会保留当前的滚动信息,保存在 history state 中,由 key 作为索引,然后在执行 handleScroll 函数,该函数主要就是获取用户的配置,执行滚动行为。而在发生浏览器行为的时候,即前进后退历史记录的时候,会发生 popstate 事件,同样会先获取到当前的滚动信息,然后看要去的历史记录是否有对应的 key,并设置 _key 为该值,然后在各个构造函数中同样监听 popstate 事件,在 onCompelete 的时候执行handleScroll 。相关代码片段如下:

carbon (4)

总结

通过对 路由懒加载 的实现到学习整个 vueRouter,收获了很多,里面还是有些许不清楚的地方,欢迎沟通交流,如有不当欢迎指出。😁😁

分享到

   
chrome扩展程序初体验
加入我们