回忆:
我们知道,h5的history或者hash帮助我们解决了,变化url跳转页面不发送请求,并且我们能监听到url变化。这只是第一步,不同url要渲染对应不同内容,才是最麻烦的问题。
我们有一个路由列表,我们会先把遍历递归它,把每个路由对象重新进行定义描述成RouteRecord对象,得到pathMap、pathList、nameMap三个数组(包括路径、名称到路由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.matcher由match方法(根据传入的路径和当前的路径,计算出新的路径)和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 个队列:updated、activated、deactivated。从当前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> 在渲染的时候,执行如下逻辑:
网友评论