Skip to Content
全部文章万论前端Vue3的路由RouterView组件是怎么工作的?

Vue3的路由RouterView组件是怎么工作的?

发布时间: 2025-04-21

前言

Vue也用了很多年了,从vue2到vue3,最近几年开始写react比较多了,vue的一些知识点逐渐在忘记,今天闲来无事,突然想去看下RouterView这个组件 是怎么工作的,尽管之前我能够通过推测是监听浏览器的window.history来实现的,现在也就是纯粹的想去看看源码,验证一下之前的推测和内部的细节 长什么样子。

Router的使用

先简单展示一下一个router是怎么配置和使用的吧,后面就是针对这个使用的每一个细节去看看对应的源码。

  1. 第一步一般都是先配置我们的router文件,里面包含了路由的path是什么,对应的渲染组件是什么。
rrouter/index.ts
import { createRouter, createWebHashHistory } from "vue-router" import { setupPermissionGuard } from './permission' // 定义router的配置 const routes = [ { path: "/", component: () => import("../view/xxxxx1.vue"), }, { path: "/system2", component: () => import("../view/xxxxx2.vue"), } ] // 创建一个router,并指定使用hash模式的路由,一般路由是#号开头的 /#/path // 还有一种使用web路由,就是不要#号,需要html5的api,也需要nginx配置支持 const router = createRouter({ history: createWebHashHistory(), routes: routes }) // 设置路由守卫 router.beforeEach(async (to, _from, next) => { return next() }) export default router;

2.然后把上面的router在main.ts中注册

main.ts
import router from "./router"; // 把router注册到vue实例内 app.use(router);

3.然后就是在需要切换路由后切换的页面的位置添加一个组件

<RouterView /> import { RouterView} from 'vue-router';

就这3步就可以当浏览器url中的path变化之后,自动去匹配我们配置的路由对应的组件。

那么,他们是如何工作的?

先看看RouterView的相关源码

这个内置的组件我们来瞅瞅里面的setup,我删掉一些非本次关注的核心逻辑,便于理解,完整源码

setup(props, { attrs, slots }) { const injectedRoute = inject(routerViewLocationKey)! const routeToDisplay = computed<RouteLocationNormalizedLoaded>( () => props.route || injectedRoute.value ) // ...省略一些代码... const matchedRouteRef = computed<RouteLocationMatched | undefined>( () => routeToDisplay.value.matched[depth.value] ) // ...省略一些代码... return () => { const route = routeToDisplay.value const currentName = props.name const matchedRoute = matchedRouteRef.value const ViewComponent = matchedRoute && matchedRoute.components![currentName] if (!ViewComponent) { return normalizeSlot(slots.default, { Component: ViewComponent, route }) } // ...省略一些代码... const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => { // remove the instance reference to prevent leak if (vnode.component!.isUnmounted) { matchedRoute.instances[currentName] = null } } const component = h( ViewComponent, assign({}, routeProps, attrs, { onVnodeUnmounted, ref: viewRef, }) ) // ...省略代码... return ( // pass the vnode to the slot as a prop. // h and <component :is="..."> both accept vnodes normalizeSlot(slots.default, { Component: component, route }) || component ) } }

从上面的逻辑,首先有一个计算属性,是用来获取当前要显示的路由

const routeToDisplay = computed<RouteLocationNormalizedLoaded>( () => props.route || injectedRoute.value )

然后setup返回了一个函数,函数里面第一行使用了这个计算属性

const route = routeToDisplay.value

下面的逻辑就是把组件拿出来去渲染,里面的normalizeSlot是一个工具函数,作用就是如果RouterView下面有子slot,会渲染子slot元素,把路由配置的 组件Component作为参数传入,比如如果你是这样用的,他也能保证渲染正常,slots.default就是内部的keep-alive组件:

<router-view v-slot="{ Component, route }"> <keep-alive> <!-- 用 fullPath 作为 key,路由变化(含参数/查询)时强制重建组件 --> <component :is="Component" :key="route.fullPath" /> </keep-alive> </router-view>

这是RouterView组件的核心渲染逻辑,你可能会想它是怎么监听到路由变化然后加载新的路由组件的?

巧妙的旧在于它满足了如下2个条件:

  • 他返回了一个函数
  • 函数内使用了injectedRoute.value

这样就会产生一个隐含的依赖关系,当依赖的数据发生变化后,函数会被重新执行。

然后来看看路由变化的流程

整体路由变化的流程是中的currentRoute.value = toLocation在修改路由,Router.ts完整源码

function finalizeNavigation( toLocation: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded, isPush: boolean, replace?: boolean, data?: HistoryState ): NavigationFailure | void { // a more recent navigation took place const error = checkCanceledNavigation(toLocation, from) if (error) return error // only consider as push if it's not the first navigation const isFirstNavigation = from === START_LOCATION_NORMALIZED const state: Partial<HistoryState> | null = !isBrowser ? {} : history.state // change URL only if the user did a push/replace and if it's not the initial navigation because // it's just reflecting the url if (isPush) { // on the initial navigation, we want to reuse the scroll position from // history state if it exists if (replace || isFirstNavigation) routerHistory.replace( toLocation.fullPath, assign( { scroll: isFirstNavigation && state && state.scroll, }, data ) ) else routerHistory.push(toLocation.fullPath, data) } // accept current navigation currentRoute.value = toLocation handleScroll(toLocation, from, isPush, isFirstNavigation) markAsReady() }

上面finalizeNavigation的调用方有2个地方,一个是setupListeners函数,一个是 pushWithRedirect 总归都是路由发生变化了才会调用改变,包括主动push、replace、前进后退等都会主动触发或者被动监听到变化。

路由守卫工作的原理

vue的路由守卫分为全局守卫、单个路由守卫,通常全局守卫来做一些全局的拦截工作,全局守卫分为

  • beforeEach:全局前置守卫,一般用来鉴权,无权限不允许跳转
  • beforeResolve:全局解析守卫,会等依赖组件准备ok了才执行,我是不怎么用
  • afterEach:全局后置守卫,一般是路由跳转完成后做一些修改title、埋点、关闭进度条之类的工作
// useCallbacks hook export function useCallbacks<T>() { let handlers: T[] = [] function add(handler: T): () => void { handlers.push(handler) // 可以在外部主动删除注册的守卫 return () => { const i = handlers.indexOf(handler) if (i > -1) handlers.splice(i, 1) } } function reset() { handlers = [] } return { add, list: () => handlers.slice(), reset, } } // 定义了3个队列管理器 const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const afterGuards = useCallbacks<NavigationHookAfter>() // 然后我们执行的 router.beforeEach() //实际就是add函数的别名,源码1003行 const router = { // ... beforeEach: beforeGuards.add, beforeResolve: beforeResolveGuards.add, afterEach: afterGuards.add, // ... }

也就是说我们执行的beforeEach就是上面useCallbacks中的add,执行后给内部的handles数组添加我们的逻辑函数

接下来就是添加的逻辑函数的执行源码了,在navigate函数跳转的时候,会调用执行所有注册的beforeGuards和beforeResolveGuards, 这里是一个链式的调用,其中guardToPromiseFn会把我们的函数拿来出,构造一个新的promise,增加一个next函数作为参数传递给我我们, 如果我们的守卫中执行了next(false),会在新的promise中触发reject。然后导致runGuardQueue出错,进而导致走到下方源码链式调用 中的catch块代码,然后跳转就相当于不执行了。

return ( runGuardQueue(guards) .then(() => { // check global guards beforeEach guards = [] for (const guard of beforeGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) // ...省略代码... .then(() => { // check global guards beforeResolve guards = [] for (const guard of beforeResolveGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) // catch any navigation canceled .catch(err => isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED) ? err : Promise.reject(err) )

最后

看了这个代码之后,我觉得有必要来好好研究一下vue的自动依赖收集,和react的hook显示的依赖确实感觉各有千秋, 暂时还说不上哪个好坏,更多的是在不同场景下的个人喜好差异。

最后编辑于

hi