《进阶》中关于VueRouter核心原理的5个问题你知道吗?
时间:2023-03-28 14:24:33
HTML
前言以问题为线索,分析了VueRouter的核心原理。源码不拆解,只用图和核心源码说明原理。本文为进阶文章。希望读者有使用Vue.js和VueRouter的经验,对Vue.js的核心原理有一个简单的了解。对应的官方仓库源码地址会放在super上,大家可以一起阅读;对应的源码版本为3.5.3,也就是对应Vue.js2.x的VueRouter的最新版本;VueRouter是一种标准的写法,为了简单起见,以下简称为router。本文将以下列问题为线索,讲解路由器的原理:this.$router,this.$route从何而来?路由器如何知道要渲染哪个组件?this.$router.push调用了什么原生API?渲染视图是什么样的?更新后的路由器如何知道切换视图?文末有大总结。以下是本文中使用的一个简单示例://main.jsimportVuefrom'vue'importAppfrom'./App'importrouterfrom'./router'newVue({el:'#app',//mountVueRouter实例router,components:{App},template:''})//router/index.jsimportVuefrom'vue'importRouterfrom'vue-router'importHomefrom'@/components/home'importAboutfrom'@/components/About'importHome1from'@/components/Home1'//使用VueRouter插件Vue.use(Router)//创建VueRouter实例exportdefaultnewRouter({routes:[{path:'/',redirect:'/home'},{path:'/home',name:'Home',component:Home,children:[{path:'home1',name:'Home1',组件:Home1}]},{path:'/about',name:'About',component:About}]})//App.vue转到主页转到关于转到Home1
页面性能示例:this.$router,this.$route从哪里来?我们使用this.$router跳转到组件中的路由,使用this.$route获取当前路由信息或者监听路由变化。那么它们是从哪里来的呢?答案是路由注册Routeregistration路由注册发生在Vue.use时,use是index.js中router暴露的VueRouter类://demo代码:importRouterfrom'vue-router'//使用VueRouter插件-在Vue中。use(Router)//index.jsimportofrouter{install}from'./install'//VueRouterclassexportdefaultclassVueRouter{}VueRouter.install=install//install.jsexportfunctioninstall(Vue){//全局混合HookfunctionVue.mixin({beforeCreate(){//有一个router配置项,代表根组件,设置根routerif(isDef(this.$options.router)){this._routerRoot=thisthis._router=this.$options.router}else{//非根组件,通过其父组件访问,逐层访问直到根组件this._routerRoot=(this.$parent&&this.$parent._routerRoot)||this}},})//添加$router和$route到Vue原型Object.defineProperty(Vue.prototype,'$router',{get(){returnthis._routerRoot._router}})Object.defineProperty(Vue.prototype,'$route',{get(){returnthis._routerRoot._route}})//router-view组件和router-link组件全局注册Vue.component('RouterView',View)Vue.component('RouterLink',Link)}所以this.$router,this.$route在注册路由的时候是和全局的beforeCreatehook混合在一起的。钩子里扩展了vue原型,router-view和router-link的来源也很清楚。我们先来看VueRouter类的核心部分。exportdefaultclassVueRouter{constructor(options){//确定路由模式,浏览器环境默认hash,Node.js环境默认abstractletmode=options.mode||'hash'this.fallback=mode==='history'&&!supportsPushState&&options.fallback!==falseif(this.fallback){mode='hash'}if(!inBrowser){mode='abstract'}this.mode=mode//根据mode实例化不同的history来管理路由开关(mode){case'history':this.history=newHTML5History(this,options.base)breakcase'hash':this.history=newHashHistory(this,options.base,this.fallback)breakcase'abstract':this.history=newAbstractHistory(this,options.base)breakdefault:if(process.env.NODE_ENV!=='生产'){assert(false,`invalidmode:${mode}`)}}}}构造函数中有两个重要的事情:1.确定路由模式,2.根据模式创建History实例。如上,history类有一个base基类,不同的模式有相应的抽象类,hash类,html5类,继承自基类,history实例处理路由切换,路由跳转等。initVueRouter的init发生在刚刚提到的beforeCreatehook中//initthis._router.init(this)在beforeCreatehook中被调用//VueRouter类的init实例方法init(app){//保存路由器实例this.app=appconsthistory=this.historyif(historyinstanceofHTML5History||historyinstanceofHashHistory){constsetupListeners=routeOrError=>{//待揭示history.setupListeners()}//路由切换history.transitionTo(history.getCurrentLocation(),setupListeners,setupListeners)}}init主要处理history.transitionTo,transitionTo调用setupListeners,先有个印象吧。路由器怎么知道渲染哪个组件用户传入路由配置后,路由器怎么知道渲染哪个组件?答案是MatcherMatcherMatcher是一个处理路由匹配的匹配器。创建匹配器发生在VueRouter类的构造函数中this.matcher=createMatcher(options.routes||[],this)//create-matcher.jsexportfunctioncreateMatcher(routes,router){//创建映射表const{pathList,pathMap,nameMap}=createRouteMap(routes)//根据我们要跳转的路由匹配到组件,比如this.$router.push('/about')functionmatch(){}}createRouteMapcreateRouteMap负责创建路由映射表导出函数createRouteMap(routes,oldPathList,oldPathMap,oldNameMap){constpathList:Array
=oldPathList||[]constpathMap:Dictionary=oldPathMap||Object.create(null)constnameMap:Dictionary=oldNameMap||Object.create(null)...return{pathList,pathMap,nameMap}}不用关注处理细节,打印例子中的路由映射表就知道里面有什么:pathList[pathlist],pathMap[pathtoRouteRecordmapping],nameMap[nametoRouteRecord'smapping]Mapping]有了路由映射表之后就很容易定位RouteRecord了。其中,路由器的部分数据结构如下:源码匹配方法match方法是取出RouterRecord//create-matcher.jsfunctionmatch(raw,currentRoute,redirectedFrom){constlocation=normalizeLocation(raw,currentRoute,false,router)const{name}=locationif(name){//name的大小写...}elseif(location.path){//path的大小写...}}什么nativeAPIthis.$router.push调用this.$router.push用于跳转路由,内部调用transitionTo来切换路由。hash模式源码和history模式源码以hash模式为例//history/hash.js//push方法push(location,onComplete,onAbort){//transitionTo做路由切换,并调用match刚才匹配器匹配路由的方法//transitionTo的第二个和第三个参数是回调函数this.transitionTo(location,route=>{pushHash(route.fullPath)onComplete&&onComplete(route)},onAbort)}//更新url,如果支持h5的pushStateapi,使用pushState方法,//否则设置window.location.hashfunctionpushHash(path){if(supportsPushState){pushState(getUrl(path))}else{window.地点。散列=路径}}functiongetUrl(path){consthref=window.location.hrefconsti=href.indexOf('#')constbase=i>=0?href.slice(0,i):hrefreturn`${base}#${path}`}history模式是调用pushState方法pushState方法源代码exportfunctionpushState(url,replace){//获取window.historyconsthistory=window.historytry{if(replace){conststateCopy=extend({},history.state)stateCopy.key=getStateKey()//调用replaceStatehistory.replaceState(stateCopy,'',url)}else{//callpushStatehistory.pushState({key:setStateKey(genStateKey())},'',url)}}catch(e){...}}router-view渲染的view是如何更新的用于渲染传入路由配置对应的组件exportdefault{name:'RouterView',functional:true,render(_,{props,children,parent,data}){...//识别data.routerView=true//通过depth从router-view组件向上遍历到根组件,//遇到其他路由depth+1的router-view组件//利用depth帮助找到对应的RouterRecordletdepth=0wh文件(parent&&parent._routerRoot!==parent){constvnodeData=parent.$vnode?parent.$vnode.data:{}if(vnodeData.routerView){depth++}parent=parent.$parent}data.routerViewDepth=depth//获取匹配组件constroute=parent.$routeconstmatched=route.matched[depth]constcomponent=matched&&matched.components[name]...//渲染对应的组件consth=parent.$createElementreturnh(component,data,children)}}比如二级路由home1在例子是二级路由,所以深度为1。找到下图中的home1组件更新,那么如何在每次路由切换毛布后触发新视图的渲染呢?每次transitionTo完成时,都会执行添加的回调函数。当前路由信息在回调函数中更新。回调注册在VueRouter的init方法中:history.listen(route=>{this.apps.forEach(app=>{//更新当前路由信息_routeapp._route=route})})并使_route响应在组件的beforeCreatehook中,在router-view的render函数中访问parent.$route,即访问到了_route,所以一旦_route发生变化,就会触发router-view组件的重新渲染//转_route进入响应式Vue.util.defineReactive(this,'_route',this._router.history.current)路由器如何知道切换视图?现在我们已经了解了路由器如何切换视图。当我们点击浏览器的后退按钮和前进按钮时,如何触发视图切换?答案是VueRouter在init的时候做了事件监听setupListenerssetupListenerspopstate事件:这个事件只会在有浏览器动作时触发,调用window.history.pushState或replaceState不会触发。文档hashchange事件:hash变化时触发的核心原理总结本文从5个问题入手,剖析VueRouter的核心原理。其他的分支,比如navigationguard是怎么实现的等等,可以自己去了解。先了解核心原理再看其他部分。这并不复杂。VueRouter更多的是如何与Vue.js的核心能力结合,应用到Vue.js的生态中。如果你想和我聊聊VueRouter原理的任何部分,可以在评论区留言。