Vue3的路由RouterView组件是怎么工作的?
发布时间: 2025-04-21
前言
Vue也用了很多年了,从vue2到vue3,最近几年开始写react比较多了,vue的一些知识点逐渐在忘记,今天闲来无事,突然想去看下RouterView这个组件 是怎么工作的,尽管之前我能够通过推测是监听浏览器的window.history来实现的,现在也就是纯粹的想去看看源码,验证一下之前的推测和内部的细节 长什么样子。
Router的使用
先简单展示一下一个router是怎么配置和使用的吧,后面就是针对这个使用的每一个细节去看看对应的源码。
- 第一步一般都是先配置我们的router文件,里面包含了路由的path是什么,对应的渲染组件是什么。
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中注册
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显示的依赖确实感觉各有千秋, 暂时还说不上哪个好坏,更多的是在不同场景下的个人喜好差异。
