vue router 源码概览案例分析
源码这个东西对于实际的工作其实没有立竿见影的效果,不会像那些针对性极强的文章一样看了之后就立马可以运用到实际项目中,产生什么样的效果,源码的作用是一个潜移默化的过程,它的理念、设计模式、代码结构等看了之后可能不会立即知识变现(或者说变现很少),而是在日后的工作过程中悄无声息地发挥出来,你甚至都感觉不到这个过程
另外,优秀的源码案例,例如vue、react这种,内容量比较庞大,根本不是三篇五篇十篇八篇文章就能说完的,而且写起来也很难写得清楚,也挺浪费时间的,而如果只是分析其中一个点,例如vue的响应式,类似的文章也已经够多了,没必要再repeat
所以我之前没专门写过源码分析的文章,只是自己看看,不过最近闲来无事看了vue-router的源码,发现这种插件级别的东西,相比vue这种框架级别的东西,逻辑简单清晰,没有那么多道道,代码量也不多,但是其中包含的理念等东西却很精炼,值得一写,当然,文如其名,只是概览,不会一行行代码分析过去,细节的东西还是要自己看看的
vue.use
vue插件必须通过vue.use进行注册,vue.use的代码位于vue源码的src/core/global-api/use.js文件中,此方法的主要作用有两个:
- 对注册的组件进行缓存,避免多次注册同一个插件
if(installedPlugins.indexOf(plugin)>-1){ returnthis }
- 调用插件的install方法或者直接运行插件,以实现插件的install
if(typeofplugin.install==='function'){ plugin.install.apply(plugin,args) }elseif(typeofplugin==='function'){ plugin.apply(null,args) }
路由安装
vue-router的install方法位于vue-router源码的src/install.js中主要是通过vue.minxin混入beforeCreate和destroyed钩子函数,并全局注册router-view和router-link组件
//src/install.js Vue.mixin({ beforeCreate(){ if(isDef(this.$options.router)){ this._routerRoot=this this._router=this.$options.router this._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) } }) ... //全局注册`router-view`和`router-link`组件 Vue.component('RouterView',View) Vue.component('RouterLink',Link)
路由模式
vue-router支持三种路由模式(mode):hash、history、abstract,其中abstract是在非浏览器环境下使用的路由模式,例如weex
路由内部会对外部指定传入的路由模式进行判断,例如当前环境是非浏览器环境,则无论传入何种mode,最后都会被强制指定为abstract,如果判断当前环境不支持HTML5History,则最终会被降级为hash模式
//src/index.js letmode=options.mode||'hash' this.fallback=mode==='history'&&!supportsPushState&&options.fallback!==false if(this.fallback){ mode='hash' } if(!inBrowser){ mode='abstract' }
最后会对符合要求的mode进行对应的初始化操作
//src/index.js switch(mode){ case'history': this.history=newHTML5History(this,options.base) break case'hash': this.history=newHashHistory(this,options.base,this.fallback) break case'abstract': this.history=newAbstractHistory(this,options.base) break default: if(process.env.NODE_ENV!=='production'){ assert(false,`invalidmode:${mode}`) } }
路由解析
通过递归的方式来解析嵌套路由
//src/create-route-map.js functionaddRouteRecord( pathList:Array, pathMap:Dictionary , nameMap:Dictionary , route:RouteConfig, parent?:RouteRecord, matchAs?:string ){ ... route.children.forEach(child=>{ constchildMatchAs=matchAs ?cleanPath(`${matchAs}/${child.path}`) :undefined addRouteRecord(pathList,pathMap,nameMap,child,record,childMatchAs) }) ... }
解析完毕之后,会通过key-value对的形式对解析好的路由进行记录,所以如果声明多个相同路径(path)的路由映射,只有第一个会起作用,后面的会被忽略
//src/create-route-map.js if(!pathMap[record.path]){ pathList.push(record.path) pathMap[record.path]=record }
例如如下路由配置,路由/bar只会匹配Bar1,Bar2这一条配置会被忽略
constroutes=[ {path:'/foo',component:Foo}, {path:'/bar',component:Bar1}, {path:'/bar',component:Bar2}, ];
路由切换
当访问一个url的时候,vue-router会根据路径进行匹配,创建出一个route对象,可通过this.$route进行访问
//src/util/route.js constroute:Route={ name:location.name||(record&&record.name), meta:(record&&record.meta)||{}, path:location.path||'/', hash:location.hash||'', query, params:location.params||{}, fullPath:getFullPath(location,stringifyQuery), matched:record?formatMatch(record):[] }
src/history/base.js源码文件中的transitionTo()是路由切换的核心方法
transitionTo(location:RawLocation,onComplete?:Function,onAbort?:Function){ constroute=this.router.match(location,this.current) this.confirmTransition(route,()=>{ ... }
路由实例的push和replace等路由切换方法,都是基于此方法实现路由切换的,例如hash模式的push方法:
//src/history/hash.js push(location:RawLocation,onComplete?:Function,onAbort?:Function){ const{current:fromRoute}=this //利用了transitionTo方法 this.transitionTo(location,route=>{ pushHash(route.fullPath) handleScroll(this.router,route,fromRoute,false) onComplete&&onComplete(route) },onAbort) }
transitionTo方法内部通过一种异步函数队列化执⾏的模式来更新切换路由,通过next函数执行异步回调,并在异步回调方法中执行相应的钩子函数(即导航守卫)beforeEach、beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
通过queue这个数组保存相应的路由参数:
//src/history/base.js constqueue:Array=[].concat( //in-componentleaveguards extractLeaveGuards(deactivated), //globalbeforehooks this.router.beforeHooks, //in-componentupdatehooks extractUpdateHooks(updated), //in-configenterguards activated.map(m=>m.beforeEnter), //asynccomponents resolveAsyncComponents(activated) )
通过runQueue以一种递归回调的方式来启动异步函数队列化的执⾏:
//src/history/base.js //异步回调函数 runQueue(queue,iterator,()=>{ constpostEnterCbs=[] constisValid=()=>this.current===route //waituntilasynccomponentsareresolvedbefore //extractingin-componententerguards constenterGuards=extractEnterGuards(activated,postEnterCbs,isValid) constqueue=enterGuards.concat(this.router.resolveHooks) //递归执行 runQueue(queue,iterator,()=>{ if(this.pending!==route){ returnabort() } this.pending=null onComplete(route) if(this.router.app){ this.router.app.$nextTick(()=>{ postEnterCbs.forEach(cb=>{cb()}) }) } }) })
通过next进行导航守卫的回调迭代,所以如果在代码中显式声明了导航钩子函数,那么就必须在最后调用next(),否则回调不执行,导航将无法继续
//src/history/base.js constiterator=(hook:NavigationGuard,next)=>{ ... hook(route,current,(to:any)=>{ ... }else{ //confirmtransitionandpassonthevalue next(to) } }) ... }
路由同步
在路由切换的时候,vue-router会调用push、go等方法实现视图与地址url的同步
地址栏url与视图的同步
当进行点击页面上按钮等操作进行路由切换时,vue-router会通过改变window.location.href来保持视图与url的同步,例如hash模式的路由切换:
//src/history/hash.js functionpushHash(path){ if(supportsPushState){ pushState(getUrl(path)) }else{ window.location.hash=path } }
上述代码,先检测当前浏览器是否支持html5的HistoryAPI,如果支持则调用此API进行href的修改,否则直接对window.location.hash进行赋值history的原理与此相同,也是利用了HistoryAPI
视图与地址栏url的同步
当点击浏览器的前进后退按钮时,同样可以实现视图的同步,这是因为在路由初始化的时候,设置了对浏览器前进后退的事件监听器
下述是hash模式的事件监听:
//src/history/hash.js setupListeners(){ ... window.addEventListener(supportsPushState?'popstate':'hashchange',()=>{ constcurrent=this.current if(!ensureSlash()){ return } this.transitionTo(getHash(),route=>{ if(supportsScroll){ handleScroll(this.router,route,current,true) } if(!supportsPushState){ replaceHash(route.fullPath) } }) }) }
history模式与此类似:
//src/history/html5.js window.addEventListener('popstate',e=>{ constcurrent=this.current //Avoidingfirst`popstate`eventdispatchedinsomebrowsersbutfirst //historyroutenotupdatedsinceasyncguardatthesametime. constlocation=getLocation(this.base) if(this.current===START&&location===initLocation){ return } this.transitionTo(location,route=>{ if(supportsScroll){ handleScroll(router,route,current,true) } }) })
无论是hash还是history,都是通过监听事件最后来调用transitionTo这个方法,从而实现路由与视图的统一
另外,当第一次访问页面,路由进行初始化的时候,如果是hash模式,则会对url进行检查,如果发现访问的url没有带#字符,则会自动追加,例如初次访问http://localhost:8080这个url,vue-router会自动置换为http://localhost:8080/#/,方便之后的路由管理:
//src/history/hash.js functionensureSlash():boolean{ constpath=getHash() if(path.charAt(0)==='/'){ returntrue } replaceHash('/'+path) returnfalse }
scrollBehavior
当从一个路由/a跳转到另外的路由/b后,如果在路由/a的页面中进行了滚动条的滚动行为,那么页面跳转到/b时,会发现浏览器的滚动条位置和/a的一样(如果/b也能滚动的话),或者刷新当前页面,浏览器的滚动条位置依旧不变,不会直接返回到顶部的而如果是通过点击浏览器的前进、后退按钮来控制路由切换时,则部门浏览器(例如微信)滚动条在路由切换时都会自动返回到顶部,即scrollTop=0的位置这些都是浏览器默认的行为,如果想要定制页面切换时的滚动条位置,则可以借助scrollBehavior这个vue-router的options
当路由初始化时,vue-router会对路由的切换事件进行监听,监听逻辑的一部分就是用于控制浏览器滚动条的位置:
//src/history/hash.js setupListeners(){ ... if(supportsScroll){ //进行浏览器滚动条的事件控制 setupScroll() } ... }
这个set方法定义在src/util/scroll.js,这个文件就是专门用于控制滚动条位置的,通过监听路由切换事件从而进行滚动条位置控制:
//src/util/scroll.js window.addEventListener('popstate',e=>{ saveScrollPosition() if(e.state&&e.state.key){ setStateKey(e.state.key) } })
通过scrollBehavior可以定制路由切换的滚动条位置,vue-router的github上的源码中,有相关的example,源码位置在vue-router/examples/scroll-behavior/app.js
router-view&router-link
router-view和router-link这两个vue-router的内置组件,源码位于src/components下
router-view
router-view是无状态(没有响应式数据)、无实例(没有this上下文)的函数式组件,其通过路由匹配获取到对应的组件实例,通过h函数动态生成组件,如果当前路由没有匹配到任何组件,则渲染一个注释节点
//vue-router/src/components/view.js ... constmatched=route.matched[depth] //renderemptynodeifnomatchedroute if(!matched){ cache[name]=null returnh() } constcomponent=cache[name]=matched.components[name] ... returnh(component,data,children)
每次路由切换都会触发router-view重新render从而渲染出新的视图,这个触发的动作是在vue-router初始化init的时候就声明了的:
//src/install.js Vue.mixin({ beforeCreate(){ if(isDef(this.$options.router)){ this._routerRoot=this this._router=this.$options.router this._router.init(this) //触发router-view重渲染 Vue.util.defineReactive(this,'_route',this._router.history.current) ... })
将this._route通过defineReactive变成一个响应式的数据,这个defineReactive就是vue中定义的,用于将数据变成响应式的一个方法,源码在vue/src/core/observer/index.js中,其核心就是通过Object.defineProperty方法修改数据的getter和setter:
Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:functionreactiveGetter(){ constvalue=getter?getter.call(obj):val if(Dep.target){ //进行依赖收集 dep.depend() if(childOb){ childOb.dep.depend() if(Array.isArray(value)){ dependArray(value) } } } returnvalue }, set:functionreactiveSetter(newVal){ ... //通知订阅当前数据watcher的观察者进行响应 dep.notify() }
当路由发生变化时,将会调用router-view的render函数,此函数中访问了this._route这个数据,也就相当于是调用了this._route的getter方法,触发依赖收集,建立一个Watcher,执行_update方法,从而让页面重新渲染
//vue-router/src/components/view.js render(_,{props,children,parent,data}){ //usedbydevtoolstodisplayarouter-viewbadge data.routerView=true //directlyuseparentcontext'screateElement()function //sothatcomponentsrenderedbyrouter-viewcanresolvenamedslots consth=parent.$createElement constname=props.name //触发依赖收集,建立renderwatcher constroute=parent.$route ... }
这个renderwatcher的派发更新,也就是setter的调用,位于src/index.js:
history.listen(route=>{ this.apps.forEach((app)=>{ //触发setter app._route=route }) })
router-link
router-link在执行render函数的时候,会根据当前的路由状态,给渲染出来的active元素添加class,所以你可以借助此给active路由元素设置样式等:
//src/components/link.js render(h:Function){ ... constglobalActiveClass=router.options.linkActiveClass constglobalExactActiveClass=router.options.linkExactActiveClass //Supportglobalemptyactiveclass constactiveClassFallback=globalActiveClass==null ?'router-link-active' :globalActiveClass constexactActiveClassFallback=globalExactActiveClass==null ?'router-link-exact-active' :globalExactActiveClass ... }
router-link默认渲染出来的元素是标签,其会给这个添加href属性值,以及一些用于监听能够触发路由切换的事件,默认是click事件:
//src/components/link.js data.on=on data.attrs={href}
另外,你可以可以通过传入tag这个props来定制router-link渲染出来的元素标签:
如果tag值不为a,则会递归遍历router-link的子元素,直到找到一个a标签,则将事件和路由赋值到这个上,如果没找到a标签,则将事件和路由放到router-link渲染出的本身元素上:
if(this.tag==='a'){ data.on=on data.attrs={href} }else{ //findthefirstchildandapplylistenerandhref //findAnchor即为递归遍历子元素的方法 consta=findAnchor(this.$slots.default) ... } }
当触发这些路由切换事件时,会调用相应的方法来切换路由刷新视图:
//src/components/link.js consthandler=e=>{ if(guardEvent(e)){ if(this.replace){ //replace路由 router.replace(location) }else{ //push路由 router.push(location) } } }
总结
可以看到,vue-router的源码是很简单的,比较适合新手进行阅读分析
源码这种东西,我的理解是没必要非要专门腾出时间来看,只要你熟读文档,能正确而熟练地运用API实现各种需求那就行了,轮子的出现本就是为实际开发所服务而不是用来折腾开发者的,注意,我不是说不要去看,有时间还是要看看的,就算弄不明白其中的道道,但看了一遍总会有收获的,比如我在看vue源码的时候,经常看到类似于这种的赋值写法:
//vue/src/core/vdom/create-functional-component.js (clone.data||(clone.data={})).slot=data.slot
如果是之前,对于这段逻辑我通常会这么写:
if(clone.data){ clone.data.slot=data.slot }else{ clone.data={ slot:data.slot } }
也不是说第一种写法有什么难度或者看不明白,只是习惯了第二种写法,平时写代码的过程中自然而然不假思索地就写出来了,习惯成自然了,但是当看到第一种写法的时候才会一拍脑袋想着原来这么写也可以,以前白敲了那么多次键盘,所以没事要多看看别人优秀的源码,避免沉迷于自己的世界闭门造车,这样才能查漏补缺,这同样也是我认为代码review比较重要的原因,自己很难发现的问题,别人可能一眼就看出来了,此之谓当局者迷旁观者清也
以上所述是小编给大家介绍的vuerouter源码概览,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!