You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Vue.mixin({beforeCreate(){if(isDef(this.$options.router)){// new Vue 实例this._routerRoot=thisthis._router=this.$options.routerthis._router.init(this)// 初始化Vue.util.defineReactive(this,'_route',this._router.history.current)}else{this._routerRoot=(this.$parent&&this.$parent._routerRoot)||this}registerInstance(this,this)},destroyed(){registerInstance(this)}})
transitionTo(location: RawLocation,onComplete?: Function,onAbort?: Function){constroute=this.router.match(location,this.current)// 根据跳转的location计算出新的routethis.confirmTransition(route,()=>{// 更新routethis.updateRoute(route)onComplete&&onComplete(route)this.ensureURL()// fire ready cbs onceif(!this.ready){this.ready=truethis.readyCbs.forEach(cb=>{cb(route)})}},err=>{if(onAbort){onAbort(err)}if(err&&!this.ready){this.ready=truethis.readyErrorCbs.forEach(cb=>{cb(err)})}})}
constcurrent=this.current;constabort=err=>{if(!isExtendedError(NavigationDuplicated,err)&&isError(err)){if(this.errorCbs.length){this.errorCbs.forEach(cb=>{cb(err);});}else{warn(false,'uncaught error during route navigation:');console.error(err);}}onAbort&&onAbort(err);};// 相同的routeif(isSameRoute(route,current)&&// in the case the route map has been dynamically appended toroute.matched.length===current.matched.length){this.ensureURL();returnabort(newNavigationDuplicated(route));}
constmatched=route.matched[depth]// render empty node if no matched routeif(!matched){cache[name]=nullreturnh()}constcomponent=cache[name]=matched.components[name]
data.registerRouteInstance=(vm,val)=>{// val could be undefined for unregistrationconstcurrent=matched.instances[name]if((val&¤t!==vm)||(!val&¤t===vm)){matched.instances[name]=val}}
// 守卫函数functionguardEvent(e){// don't redirect with control keysif(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)return// don't redirect when preventDefault calledif(e.defaultPrevented)return// don't redirect on right clickif(e.button!==undefined&&e.button!==0)return// don't redirect if `target="_blank"`if(e.currentTarget&&e.currentTarget.getAttribute){consttarget=e.currentTarget.getAttribute('target')if(/\b_blank\b/i.test(target))return}// this may be a Weex event which doesn't have this methodif(e.preventDefault){e.preventDefault()}returntrue}
前端路由是构建单页面应用的关键技术,它可以让浏览器URL变化但是不请求服务器的前提下,让页面重新渲染出我们想要的结果。
Vue-Router
是Vue应用的前端路由插件,让我们来看看它的实现原理。路由例子
为了可以更好的阅读源码,我们可以用一个路由的简单例子来看下关键步骤的结果:
对应App组件的代码:
插件安装
Vue为所有插件提供一个
Vue.use()
来安装注册插件,这个方法会调用插件导出对象的install
方法,并把Vue函数作为该函数第一个参数传递。在src/install.js
文件中是关于Vue Router的安装程序。安装的过程主要有关键的几步:Vue.mixin()
全局混入beforeCreate
和destroyed
钩子:在
beforeCreate
钩子中,对于根实例,设置_routerRoot
为实例本身,_router
为VueRouter实例并调用init
方法进行初始化,然后通过defineReactive
把_route
属性进行响应处理,这个是路径导航导致视图渲染的关键。对于组件实例,通过父子链关系this.$parent && this.$parent._routerRoot
设置_routerRoot
属性。在函数最后调用registerInstance
主要是把组件的实例和路由规则进行绑定,这个之后会知道用处。Vue.prototype
挂载属性这就是为什么我们能在每个组件内通过
vm.$router
和vm.$route
方法路由实例和当前路由路径的原因之后我们就可以在任何组件内使用
router-link
进行路由跳转,使用router-view
进行路由组件的挂载。VueRouter
在进行插件安装后,然后会声明路由配置规则,并通过
new VueRouter(options)
新建路由实例。在src/index.js
定义VueRouter类,在构造函数中先初始化一些属性:紧接着是对路由模式
mode
的处理:它默认值为
hash
。对于设置了history
模式,并且浏览器不支持history.pushState
并且没有设置不允许自动降级fallback=false
,会自动用hash
模式替换。如果不在浏览器端会采用abstract
,比如在node环境,它主要是用数组的方式来模拟浏览记录栈。最后根据不同的mode
来新建History实例,它是路由切换和记录管理的类:它们都定义在
src/history
文件下,不同模式的histroy类都继承定义在base.js
的基类。init
在之前路由安装中会通过在全局混入
beforeCreate
钩子并组件根实例会调用router
实例的init
方法:在
init
方法首先是对注册路由的Vue实例的管理:在app有值的前提下,会直接返回。这是为了让路由切换相关事件的绑定只处理一次:
因为
history
在构造阶段已经根据不同mode
初始化了,所以它调用不同方式进行初始根路由的跳转。transitionTo
是跳转到指定路由位置的入口,setupListeners
方法是监听浏览器的url变化,里面实现细节我们后文会分析。matcher
路由实例有一个关键的属性
matcher
,它表示一个路由匹配器,是对路由记录和新路由匹配的管理。在构造函数中通过createMatcher
方法进行初始化:这个方法定义在
src/create-matcher.js
,它定义了一些处理路由记录的方法并最终导出一个具有match
和addRoutes
方法的对象:addRoutes
方法是添加路由配置对象,因为我们开发过程中并不是所有路由规则都是事先定义好的。主要是调用createRouteMap
方法把routes
路由配置规则生成路由记录并存在对应的变量中。路由记录RouteRecord
是对我们路由配置对象的格式化对象,它的类型定义:来看下
createRouteMap
方法定义:这里一开始获取传入的存取对象否则新建一些默认值,这就是我们在之后可以添加路由配置的关键。接着遍历每个路由规则配置对象调用
addRouteRecord
方法。这个方法就是生成一个路由记录对象并存在对应的位置对于嵌套的路由规则,还要遍历
chidlren
中每个路由规则并递归调用addRouteRecord
方法:最终,
pathList
就是所有路由规则路径的数组,pathMap
就是路径到路由记录的映射,nameMap
就是路由名称到路由记录的映射。对于我们的例子,生成的结果如下:
接着,我们看下
match
方法的定义。match
方法是根据当前的route
和响应跳转的位置location
计算得出新的route
。首先,要对新的跳转位置进行格式化:因为我们在
route-link
的to
属性或者router.push
方法传的可以是一个字符串或者对象,统一把它格式化成location
对象。对于它的类型定义:对应有
name
属性的location
,我们直接在nameMap
中可以拿到对应的路由记录。然后处理下路由参数生成路由最终的路径:对于只有
path
属性的位置信息,必须遍历每个记录进行正则匹配,匹配到了还要提取path
中的参数到params
对象中:两种情况找到对于的路由记录后都会调用
_createRoute
方法生成路由对象route
:其中
redirect
和alias
是对重定向和别名的处理,在它们里面或者正常情况都会调用createRoute
来返回一个路由路径对象。路由对象
route
和位置信息location
最大区别它包含一个路由规则匹配的路径信息matched
。它通过formatMatch
方法生成:很明显它会通过不断查找记录的父亲,然后丢到数组头部,所以
matched
的匹配顺序保持先父后子。这个属性在后面的视图渲染尤其重要。最终,我们的match
方法就根据新位置信息和当前路由对象得出新的路由对象。history
在路由的编程导航中,我们可以通过
router.push()
方法来跳转一个新的路径并渲染新的视图。我们从入口看下整个流程。push
是不同类型的history
自身的实现,对于HashHistory
的定义在src/history/hash.js
:这个方法主要调用了基类定义的
transitionTo
方法:导航守卫
在
transitionTo
方法中先调用match
方法计算出新的路由对象,然后调用confirmTransition
方法,这个方法主要是处理导航守卫的逻辑。首先会判断新的路由对象是否和老路由对象相等,相等的话会调用aboart
函数并退出:接着根据新老路由对象计算出需要更新,进入和离开的路由记录:
resolveQueue
方法的定义:有了这三个变量我们就可以轻松提取路由记录对应的路由钩子,包括离开,更新或者进入的钩子:
这个钩子函数数组会作为
runQueue
函数的参数进行调用:这个方法从零开始遍历路由钩子,对于每个钩子都会作为
fn
函数参数调用,并且在第二个函数参数中递归调用进行下个钩子的执行逻辑,很明显第二个参数就类似我们钩子函数的next
。fn
是一个定义的迭代器来执行钩子函数:这个函数很简单,它先执行我们的路由钩子函数,然后判断传给
next
函数的参数,如果是false
的情况直接调用abort
函数中止导航,如果是字符串还要进行跳转到新的路径,最后才成功执行下一个钩子函数。在runQueue
方法的钩子队列执行完后会执行下面的回调逻辑:其中
postEnterCbs
是对beforeRouteEnter
钩子中传给next
回调参数的处理,它会在$nextTick
视图渲染后执行,这个时候就能获取对应组件的实例对象。在执行完全局的beforeresolve
钩子后,会先执行onComplete(route)
方法执行成功的回调。在这个方法中会执行updateRoute
来更新路由:这个方法先更新
this.current
为新的路由对象,然后执行cb函数来设置注册路由的app的_route
方法。它在init
方法中进行定义:因为在app根实例中对
_route
进行了响应式处理,所以重新设置就会触发它的setter
,从而就会触发它的依赖进行更新,至于这些依赖其实就是用到route-view
组件的实例的渲染watcher
,然后这些实例就会重新执行render
函数,根据最新的route
对象来计算要渲染的路由组件,最后路由出口挂载最新的视图。在updateRoute
方法最后,会执行全局的afterEach
钩子。url更新
在执行
updateRoute
方法后,还会执行传给transitionTo
方法的成功回调。对于HashHistory对象,会执行pushHash
方法来更新最新的路由路径:这个方法先判断浏览器是否支持
pushState
,支持的话通过getUrl
方法得到完成的url并执行pushState
设置。否则利用window.location.hash
来设置浏览器url的hash:这样当我们进行路由导航,浏览器的url就会更着变化。还有一种情况就是我们手动输入url,要监听响应的事件进行路由导航更新视图,对于hash类型它是在
init
中的setupListeners
函数进行绑定:其中
ensureSlash
函数是处理不存在任何路径的情况,它会自动替换成#/
:路由组件
router-view
Vue会在
router-view
组件中根据当前的路由对象渲染出正确的组件,那这一过程是怎么实现的。直接来看下这个组件的定义的渲染函数:因为
router-view
是一个函数组件,它在渲染的过程中是不存在组件实例的。对比正常的组件渲染,它在组件占位节点的init
钩子中新建组件实例,然后调用实例的render
函数进行渲染。函数组件的render
函数是在创建占位节点时候就直接执行了,也就是说当前的实例就是在route-view
组件的环境,也就是传给函数组件render
函数第二个参数的parent
属性。类比我们的例子,第一个
route-view
执行时的parent
就是App组件的实例,第二个就是Foo组件的实例。紧接着是计算当前
router-view
的深度,它是判断当前所在的组件的占位符vnode是否有routerView
属性来进行判断:比如我们例子的第二个
router-view
所在组件是Foo,所以parent.$vnode
就是Foo组件的占位vnode,因为它是挂载在第一个router-view
的,所以它的routerView
为true
,于是depth为1;在求出深度后,就可以在当前
route
对象的匹配路径中的matched
属性得到渲染的组件对象components
,然后根据具体的视图名称拿到对应的组件对象:然后在要挂载的组件占位vnode上的
data
设置registerRouteInstance
函数,这个函数会在组件的beforeCreate
钩子中调用,并且设置匹配到的路由记录的instances
属性,这样才能在路由的beforeRouteEnter
中拿到对应的组件实例:最后返回将要挂载的组件的占位vnode:
因为我们在
router-view
的render
函数中用到了vm.$route
属性,所以当前实例的渲染watcher
会作为依赖进行收集,也就是说router-view
组件所在的环境实例的render
函数在路由切换时会重新执行。也就是为什么当我们进行路由导航时视图会更新的原因。router-link
router-link
组件是用来进行跳转的全局组件,它内部的实现原理也是调用了router.push
路由方法进行导航。来看下render
函数:首先调用
router.resolve
方法计算出新的路由对象:然后是一堆处理点击连接样式的。接着定义一个守卫函数
guardEvent
,它会在某些情况直接返回不进行导航:然后定义连接点击的处理函数,它会根据配置判断调用
push
还是replace
方法:对于
tag
不是a
标签的情况,还要处理slot
里面是否有a
标签的情况,有的话把data
附加到它身上,否则赋值在最外层上。最后返回tag
创建的vnode:总结
到此,
Vue-Router
的源码就大致分析完了,其实里面有很多实现细节还没扣,比如怎么格式化一个location
,怎么提取对应组件的路由钩子,滚动的处理等,但是这不影响路由变更到视图渲染的主流程。整个流程可以概括下图:The text was updated successfully, but these errors were encountered: