前言【vue-router源码】系列文章将带你从0开始了解vue-router的具体实现。本系列文章源码参考vue-routerv4.0.15。源码地址:https://github.com/vuejs/router阅读本文的前提是你最好了解vue-router的基本使用。如果没有使用过,可以通过vue-router官网进行学习。本文将分析RouterView组件的实现。UseRouterViewexportconstRouterViewImpl=/*#__PURE__*/defineComponent({name:'RouterView',inheritAttrs:false,props:{//如果设置了name,渲染相应路由配置下的组件Correspondingcomponentname:{type:StringasPropType,default:'default',},route:ObjectasPropType,},//为@vue/compat//提供更好的兼容性//https://github.com/vuejs/router/issues/1315compatConfig:{MODE:3},setup(props,{attrs,slots}){//如果的父节点是或Prompt__DEV__&&warnDeprecatedUsage()//当前路由constinjectedRoute=inject(routerViewLocationKey)!//要显示的路由,先走props.routeconstrouteToDisplay=computed(()=>props.route||injectedRoute.value)//router-view的深度,从0开始constdepth=inject(viewDepthKey,0)//要显示的路由constmatchedRouteRef=computed(()=>routeToDisplay.value.matched[depth])提供(viewDepthKey,depth+1)提供(matchedRouteKey,matchedRouteRef)提供(routerViewLocationKey,routeToDisplay)constviewRef=ref()watch(()=>[viewRef.value,matchedRouteRef.value,props.name]asconst,([instance,to,name],[oldInstance,from,oldName])=>{if(to){//当导航到新路由时,更新组件instanceto.instances[name]=instance//组件实例应用于不同的路由if(from&&from!==to&&instance&&instance===oldInstance){if(!to.leaveGuards.size){to.leaveGuards=from.leaveGuards}if(!to.updateGuards.size){to.updateGuards=from.updateGuards}}}//触发beforeRouteEnter下一个回调if(instance&&to&&(!from||!isSameRouteRecord(到,从)||!oldInstance)){;(to.enterCallbacks[name]||[]).forEach(callback=>callback(instance))}},{flush:'post'})return()=>{constroute=routeToDisplay.valueconstmatchedRoute=matchedRouteRef.value//要显示的组件constViewComponent=matchedRoute&&matchedRoute.components[props.name]constcurrentName=props.name//如果找不到对应的组件,则使用默认槽if(!ViewComponent){returnnormalizeSlot(slots.default,{Component:ViewComponent,route})}//在路由中定义propsconstroutePropsOption=matchedRoute!.props[props.name]//如果routePropsOption为空,取null//如果routePropsOption为真,取route.params//如果routePropsOption是函数,取函数的返回值//否则取routePropsOptionconstrouteProps=routePropsOption?routePropsOption===真?route.params:typeofroutePropsOption==='函数'?routePropsOption(route):routePropsOption:null//当组件实例被卸载时,删除组件实例以防止泄漏constonVnodeUnmounted:VNodeProps['onVnodeUnmounted']=vnode=>{if(vnode.component!.isUnmounted){matchedRoute!.instances[currentName]=null}}//生成组件constcomponent=h(ViewComponent,assign({},routeProps,attrs,{onVnodeUnmounted,ref:viewRef,}))if((__DEV__||__FEATURE_PROD_DEVTOOLS__)&&isBrowser&&component.ref){//...}return(//如果有默认插槽,使用默认插槽,否则使用组件normalizeSlot(slots.default,{Component:component,route})||component)}},})为了更好的理解router-view的渲染过程,我们看下面的例子:首先指定我们的路由表如下:constrouter=createRouter({//...//Home和Parent是两个简单的组件路由:[{名称:'Home',路径:'/',组件:Home,},{name:'Parent',path:'/parent',component:Parent,},]})假设我们的地址是http://localhost:3000现在我们访问http://localhost:3000,你肯定可以想到router-视图中的显示必须是Home组件。那么它是如何渲染的呢?首先我们要知道,vue-router在安装的时候,会进行第一次路由跳转,并立即给app注入一个默认的currentRoute(START_LOCATION_NORMALIZED)。这个时候router-view会根据这个currentRoute进行第一次渲染。因为默认currentRoute中的matched是空的,所以第一次渲染的结果是空的。第一次路由跳转完成后,会执行一个finalizeNavigation方法,在该方法中更新currentRoute。此时在currentRoute中可以找到需要渲染的组件Home,router-view完成二次渲染。第二次渲染完成后,立即触发router-view中的watch,将最新的组件实例赋值给to.instance[name],并循环执行to.enterCallbacks[name](通过使用next()inhook添加函数,流程结束,然后我们从http://localhost:3000跳转到http://localhost:3000/parent,假设使用push跳转,跳转完成后会执行finalizeNavigation,并且currentRoute会被更新,其中router-view监听currentRoute的变化,找到需要渲染的组件,并显示出来,在渲染前,执行老组件卸载钩子,并重置路由对应的实例tonull渲染完成后,触发watch更新最新的赋值给to.instance[name]的组件实例,循环执行to.enterCallbacks[name],流程结束。在之前分析router.push的过程中,我们曾经得到一个不完整的导航分析过程,所以这里可以补全:触发导航调用inactivated组件中的beforeRouteLeave钩子调用全局的beforeEach钩子调用reusebeforeRouteUpdate组件中的钩子调用路由配置中的beforeEnter钩子来解析异步路由组件。在激活组件中调用beforeRouteEnter钩子。调用全局beforeResolve挂钩。导航已确认。调用全局afterEach挂钩。一个好的组件实例将作为回调函数的参数传入。Summaryrouter-view根据currentRoute和depth找到匹配的路由,然后根据props.name和slots.default确定要显示的组件。