Vue-Router

回忆:

        我们知道,h5的history或者hash帮助我们解决了,变化url跳转页面不发送请求,并且我们能监听到url变化。这只是第一步,不同url要渲染对应不同内容,才是最麻烦的问题。

        我们有一个路由列表,我们会先把遍历递归它,把每个路由对象重新进行定义描述成RouteRecord对象,得到pathMappathListnameMap三个数组(包括路径、名称到路由RouteRecord的映射关系)。提供了addRoutes方法方便我们可以对这3个数组做动态修改。提供一个match方法,我们可以通过传入的位置Location和当前路径RouteRecord计算出新的位置并匹配到对应的路由RouteRecord,然后根据新的位置和RouteRecord计算出一个新路径Route对象。Route对象有一个matched属性,值是一个数组,把当前路径匹配到的RouteRecord,往上遍历它的parent,直到最根路径,所有RouteRecord都可以先父后子保存到这个数组中。

        我们在做路径切换transitionTo时,先通过match(输入位置,当前路径RouteRecord)方法得到新Route对象。然后通过confirmTransition(Route对象,成功回调,失败回调)完成一次路径切换。会根据当前路径的Route对象的matched属性值和目标路径的Route对象的matched属性值,找到两者数组中length长度最长的值,遍历它,找到第一个不同点,得到updated(目标RouteRecord和当前RouteRecord相同,前面重复的部分)、activated(目标RouteRecord和当前RouteRecord不同,后面不同的部分)deactivated(当前RouteRecord和目标RouteRecord不同,后面不同的部分)三个RouteRecord数组。后面导航守卫有用。

        一、导航守卫,就是在路劲切换时候执行了一系列钩子函数。

        1、通过deactivated数组去得到一个个即将离开的失活组件内的beforeRouteEnter函数,先子后父去调用他们(1、不能获取组件实例this。2、在渲染组件的对应路由被confirm前调用。3、当守卫执行前,组件实例还没有被创建。)

        2、通过this.router.beforeHooks调用全局定义beforeEach守卫。

        3、通过updated数组去得到在重用的组件里调用的beforeRouteUpdate守卫。

        4、通过调用activated.map(m => m.beforeEnter),调用激活的路由配置中定义的 beforeEnter 函数。

        5、解析异步路由组件。6、在被激活的组件里调用beforeRouteEnter。7、调用全局的beforeResolve守卫。8、导航被确认。9、调用全局的afterEach钩子。10、触发DOM更新。11、用创建好的实例调用beforeRouteEnter守卫中传给next回调函数。

        二、路径切换中,url的变化(我们分析hash模式)。当我们点击<router-link>时,会去执行history的push方法。实际上会去执行transitionTo做一次路径切换,在切换的成功回调中会执行pushHash(route.fullPath全路径)做url变化当浏览器回退为什么会触发路径变换?因为在我们初始化时会做一次路径变换,成功回调会初始化监听器(popstate或者hashchange)。在hash模式下为什么会自动给url路径添加“#”?在index.js中History实例化过程中,会去做到。

        三、组件渲染。首先<router-view>是函数式组件,和普通render函数不同,它支持第二个参数{props,children,parent,data}(和component.options相似)。执行var h = parent.$creatElement方法(对于<router-view>来说,parent就是它占位符所在位置的组件的实例)。

        通过props.name取到跳转路由的name属性。

        取到parent.$route(路由初始化beforeCreate时,会把 根Vue._route设为响应式,值为this._router.history.current。若为根vue,把this._routerRoot设为它自己,若为非根vue,this._routerRoot指向根vue。给Vue原型上定义了$route,默认值为this._routerRoot._route,所以非根Vue取$route会去取根vue的this._routerRoot._route,得到this._router.history.current当前路径对象并触发其响应式get函数)。

        我们怎么知道<router-view>渲染什么组件?<router-view>的层级关系和路由表的嵌套关系就是一一映射的关系,我们需要根据路由表的嵌套关系去找到<router-view>渲染的组件。在<router-view>函数组件中,我们会往上循环parent直到根vue,如果它是一个嵌套的<router-view>(即它占位符所在位置的组件的实例也是<router-view>渲染出来的),会对depth(深度记录)++,也就得到了当前<router-view>的路由深度。

        这个depth有什么用呢?我们在做路径转换去根据输入位置和当前路径得到新路径时得到的Route对象中的matched属性值是 根路由到当前路由的层级routeRecord记录。const matched = route.matched[depth].components.name就得到了当前路由对应的组件。这样我们就知道<router-view>对应渲染什么组件。

        我们导航钩子执行中会去通过bindGuard去给钩子函数绑定上下文,绑定的上下文为对应组建实例(通过routedRecord的instance属性值取到)。routeRecord对象的components属性值拿到的是组件实例的options。那instance属性值(组件实例)是怎么拿到的?我们在每个组件组件初始化beforeCreated时,会执行registerInstance(vm.$options._parentVnode.data.registerRouteInstance(vm)存在的话会执行),而在<router-view>函数中,会给其定义data.registerRouteInstance(vm)方法(给当前路由routeRecord的instance属性值赋值vm组件实例)。

        路径切换的时候为什么会执行<router-view>对应的render函数?前面分析过<view-router>函数,获取parent.$route时,会去取根vue的this._routerRoot._route,触发其响应式get函数,进行订阅者收集并更新视图。我们会去监听history变化然后改变Vue._route,触发视图重新渲染(当transitionTo路径切换完,history变化)。

        <router-link>也是函数式组件,props中一系列属性在我们写router-link时可以传入。render函数逻辑,就是处理props中属性(tag,activeClass等),最后切换url。

总结:

        路由始终会维护当前的线路,路由切换的时候会把当前路线切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改url,同样会渲染对应组件,切换完毕会把目标路线更新替换当前的线路,作为下一次路径切换的依据。

概述:

        Vue-Router是Vue.js官方提供的路由解决方案,它的作用就是根据不同路径映射到不同的视图

        它非常强大,支持hash、history、abstract3种路由方式,提供了<router-link>和<router-view>2种组件,还提供了简单的路由配置和一系列好用的API。

一、Vue.use,插件注册原理

        当我们 import VueRouter from 'vue-router'时,VueRouter得到的是什么?Vue-router源码中,在package.json中moudule的定义是WebPack3对Vue-router打包后的源码。在build/configs.js中的genConfig中定义了打包的入口文件。由此我们知道,入口文件在src/index.js中。从入口文件得知,我们得到的是一个VueRouter的Class(类)

        Vue.use定义Vue源码的src/core/global-api中的use.js中,Vue.use=function(plugin){...}。它主要做了两件事:

        1、管理注册。通过installedPlugins来管理注册过的组件,防止组件多次注册

        2、在插件内拿到Vue去做一些事。拿到Vue.use(plugin,...)中plugin后面的参数(toArray(arguments,1))作为数组args,把Vue添加到args数组头部(之后插件会通过Vue的属性方法去做一些事情)。然后判断plugin.install是否是函数(typeof ... === 'function' )?是的话,plugin.install.apply(plugin, argus)。否的话,plugin.apply(null, args)。所以通常我们都会为一个插件编写一个install方法。

        VueRouter中的install方法(vue-router源码的src/install)。

vue-router的install方法

        install方法,1、通过installed和Vue判断是否已经进行过install方法进行注册,进行过就return,不会进行多次注册。2、通过Vue.mixin(options)把mixin扩展到全局Vue的options中,这样每个组件的beforeCreaed和destory钩子函数里都会有这里定义的逻辑。3、在Vue原型上定义了$router和$route两个属性,返回值为this._routerRoot._router和this._routerRoot._route(this指代取该值的上下文)。4、注册RouterView和RouterLink两个组件

二、var routes = new VueRouter({...}),new Vue({routes}),对路由进行实例化,并传入全局Vue。我们看看实例化路由时做了什么操作?1、通过createMatcher(options.routes || [], this)得到的this.matchermatch方法(根据传入的路径和当前的路径,计算出新的路径)和addRoutes方法(根据路由列表,创建一个路由映射表)组成。2、对路由模式进行了判断,实例化相应路由实例HTML5History/HashHistory/AbstractHistory,它们都继承于History类。  

        createMatcher初始化就是根据路由的配置描述创建映射表,包括路径、名称到路由record的映射关系。它提供一个match方法 ,会根据传入的位置和路径计算出新的位置,并匹配到对应的路由record,然后根据新的位置和record创建新的路径并返回

三、我们通过import..拿到VueRouter(class),又通过Vue.use(VueRouter)注册了插件。接下来是插件在组件中的初始化。

        install时通过Vue.mixin对全局Vue扩展了两个钩子函数beforeCreate和destroyed。初始化就在beforeCreate时进行,1、如果是根Vue,调用this._router.init()方法,否则把子组件的this._routerRoot指向根Vue的_routerRoot。1.1、this._router.init()又会去执行transitionTo方法做路径切换(还有history的push和replace都会触发路径切换)

1、了解导航守卫的执行逻辑。导航守卫实质是在路径切换过程中执行了一些钩子函数。

路径切换

      1、 根据目标location和当前路径,生成新路径。 transitionTo 首先根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径。这里 this.current 是 history 维护的当前路径, this.current 的初始值是在 history的构造函数中初始化的:this.current=START,export const START=createRoute(null,{path:'/'})。这样就创建了一个初始的 Route,而 transitionTo 实际上也就是在切换 this.current,稍后我们会看到。

        2、做路径切换。拿到新的路径后,那么接下来就会执行 confirmTransition 方法去做真正的路径切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有成功回调函数和失败回调函数。

        confirmTransition。1、定义了 abort 函数(取消跳转调用),然后判断如果计算后的 route 和 current 是相同路径的话,调用 this.ensureUrl(之后介绍) 和 abort。2、根据 current.matched 和 route.matched 执行了 resolveQueue 方法解析出 3 个队列updatedactivateddeactivated。从当前route、新route开始往上查找父routeRecord,直到根routeRecord,形成2个数组。从头对比这两个数组,直到第一个不同点,取到其index。updated:是目标切换route和当前route相同,前面重复的部分。activated:是目标切换route和当前route不同,后面不同的部分。deactivated:是当前route和目标切换route不同,后面不同的部分。后面需要根据3个队列判断哪些守卫导航需要执行

        3、导航守卫。实际上就是发生在路由路径切换时,执行的一系列钩子函数。从整体上看一下这些钩子函数执行的逻辑,首先构造一个队列 queue,它实际上是一个数组;然后再定义一个迭代器函数 iterator;最后再执行 runQueue 方法执行这个队列。

这是一个非常经典的异步函数队列化执行的模式, queue 是一个 NavigationGuard 类型的数组,我们定义了 step 函数,每次根据 index 从 queue 中取一个 guard,然后执行 fn 函数,并且把 guard 作为参数传入,第二个参数是一个函数,当这个函数执行的时候再递归执行 step 函数,前进到下一个,注意这里的 fn 就是我们刚才的 iterator 函数
iterator 函数逻辑很简单,它就是去执行每一个 导航守卫 hook,并传入 route、current 和匿名函数,这些参数对应文档中的 to、from、next,当执行了匿名函数,会根据一些条件执行 abort 或 next,只有执行 next 的时候,才会前进到下一个导航守卫钩子函数中,这也就是为什么官方文档会说只有执行 next 方法来 resolve 这个钩子函数。
queue 是怎么构造的:1、在失活的组件里调用离开守卫。2、调用全局的 beforeEach 守卫。3、在重用的组件里调用 beforeRouteUpdate 守卫。4、在激活的路由配置里调用 beforeEnter。5、解析异步路由组件。

        第一步,在失活的组件里调用离开守卫

执行extractGuards,传入当前路由(即将离开)的deactive(比较出来不同的即将不要的routeRecord),beforeRouteLeave是在组件中定义的。
我们要去得到失活组件的离开守卫,并把它们排成列表 先子后父
去得到离开守卫列表
去组件中得到守卫函数
把守卫函数的上下文绑定对应组件实例进行执行

        flatMapComponents做的事情,通过从上面切换路径时confirmTransition得到的deactive数组,去得到与组件相关的参数。然后调用作为参数传入的fn函数,把前面得到的参数传给fn函数。fn调用完毕即得到守卫列表。

        第二步,调用用户注册的全局 beforeEach 守卫

当我们使用 router.beforeEach 注册了一个全局守卫,就会往 router.beforeHooks 添加一个钩子函数,这样 this.router.beforeHooks 获取的就是用户注册的全局 beforeEach 守卫。

        list是router实例的beforeHooks钩子函数数组fn是我们调用beforeEach时自定义回调函数。

        第三步extractUpdateHooks(updated)。调用所有重用的组件中定义的 beforeRouteUpdate 钩子函数

和 extractLeaveGuards(deactivated) 类似都是调用extractGuards,只不过传入的数据是updated,name为beforeRouteUpdate

        第四步,执行 activated.map(m => m.beforeEnter),调用激活的路由配置中定义的 beforeEnter 函数。

路由中定义的

        第五步,执行 resolveAsyncComponents(activated) 解析异步组件。

        resolveAsyncComponents 返回的是一个导航守卫函数,有标准的 to、from、next 参数。它的内部实现很简单,利用了 flatMapComponents 方法从 matched 中获取到每个组件的定义,判断如果是异步组件,则执行异步组件加载逻辑,这块和我们之前分析 Vue 加载异步组件很类似,加载成功后会执行 match.components[key] = resolvedDef 把解析好的异步组件放到对应的 components 上,并且执行 next 函数。

        这样在 resolveAsyncComponents(activated) 解析完所有激活的异步组件后,我们就可以拿到这一次所有激活的组件。这样我们在做完这 5 步后又做了一些事情

后续

        第六步,在被激活的组件里调用 beforeRouteEnter

        第七步,调用全局的 beforeResolve 守卫。

        第八步,调用全局的 afterEach 钩子。

        那么至此我们把所有导航守卫的执行分析完毕了,我们知道路由切换除了执行这些钩子函数,从表象上有 2 个地方会发生变化,一个是 url 发生变化,一个是组件发生变化。接下来我们分别介绍这两块的实现原理。

2、了解url的变化逻辑。

        当我们点击 router-link 的时候,实际上最终会执行 router.push(this.history.push),我们介绍一下hash History的push实现。

hash History的push

        会先通过transitionTo做路径切换,成功的回调会执行pushHash(route.fullPath)方法(url相关),handleScroll(滚动条相关)。

pushHash
判断是否支持h5的pushState
如果支持h5的pushState去执行replace或者pushState

然后在 history 的初始化中,会设置一个监听器,监听历史栈的变化:

当点击浏览器返回按钮的时候,如果已经有 url 被压入历史栈,则会触发 popstate 事件,然后拿到当前要跳转的 hash,执行 transtionTo 方法做一次路径转换。

3、了解组建渲染的逻辑。

        路由最终的渲染离不开组件,Vue-Router 内置了 <router-view> 组件,它的定义在 src/components/view.js 中。<router-view> 是一个 functional 组件,它的渲染也是依赖 render 函数,那么 <router-view>具体应该渲染什么组件呢?

        首先获取当前的路径:const route=parent.$route。在 src/install.js 中,我们给 Vue 的原型上定义了 $route。

        然后在 VueRouter 的实例执行 router.init 方法的时候,会执行如下逻辑,定义在 src/index.js中:

把app实例的_route和路径对应起来
history.listen
在 updateRoute 的时候执行 this.cb

        也就是我们执行 transitionTo 方法最后执行 updateRoute 的时候会执行回调,然后会更新所有组件实例的 _route 值,所以说 $route 对应的就是当前的路由线路。

        <router-view> 是支持嵌套的,回到 render 函数,其中定义了 depth 的概念,它表示 <router-view> 嵌套的深度。每个 <router-view> 在渲染的时候,执行如下逻辑:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容