【vue-rouer源码】系列文章
- 【vue-router源码】一、router.install解析
- 【vue-router源码】二、createWebHistory、createWebHashHistory、createMemoryHistory源码解析
- 【vue-router源码】三、理解Vue-router中的Matcher
- 【vue-router源码】四、createRouter源码解析
- 【vue-router源码】五、router.addRoute、router.removeRoute、router.hasRoute、router.getRoutes源码分析
- 【vue-router源码】六、router.resolve源码解析
- 【vue-router源码】七、router.push、router.replace源码解析
- 【vue-router源码】八、router.go、router.back、router.forward源码解析
- 【vue-router源码】九、全局导航守卫的实现
- 【vue-router源码】十、isReady源码解析
- 【vue-router源码】十一、onBeforeRouteLeave、onBeforeRouteUpdate源码分析
- 【vue-router源码】十二、useRoute、useRouter、useLink源码分析
- 【vue-router源码】十三、RouterLink源码分析
前言
【vue-router源码】系列文章将带你从0开始了解vue-router 的具体实现。该系列文章源码参考vue-router v4.0.15 。 源码地址:https://github.com/vuejs/router 阅读该文章的前提是你最好了解vue-router 的基本使用,如果你没有使用过的话,可通过vue-router官网学习下。
该篇文章将分析onBeforeRouteLeave 、onBeforeRouteUpdate 的实现。
使用
onBeforeRouteLeave 、onBeforeRouteUpdate 是vue-router 提供的两个composition api ,它们只能被用于setup 中。
export default {
setup() {
onBeforeRouteLeave() {}
onBeforeRouteUpdate() {}
}
}
onBeforeRouteLeave
export function onBeforeRouteLeave(leaveGuard: NavigationGuard) {
if (__DEV__ && !getCurrentInstance()) {
warn(
'getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function'
)
return
}
const activeRecord: RouteRecordNormalized | undefined = inject(
matchedRouteKey,
{} as any
).value
if (!activeRecord) {
__DEV__ &&
warn(
'No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside of a component child of <router-view>. Maybe you called it inside of App.vue?'
)
return
}
registerGuard(activeRecord, 'leaveGuards', leaveGuard)
}
因为onBeforeRouteLeave 是作用在组件上的,所以onBeforeRouteLeave 开头就需要检查当前是否有vue 实例(只在开发环境下),如果没有实例进行提示并return 。
if (__DEV__ && !getCurrentInstance()) {
warn(
'getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function'
)
return
}
然后使用inject 获取一个matchedRouteKey ,并赋给一个activeRecord ,那么个activeRecord 是个什么呢?
const activeRecord: RouteRecordNormalized | undefined = inject(
matchedRouteKey,
{} as any
).value
要想知道activeRecord 是什么,我们就需要知道matchedRouteKey 是什么时候provide 的。因为onBeforeRouteLeave 式作用在路由组件中的,而路由组件一定是RouterView 的子孙组件,所以我们可以从RouterView 中找一下答案。
在RouterView 中的setup 有这么几行代码:
setup(props, ...) {
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed(() => props.route || injectedRoute.value)
const depth = inject(viewDepthKey, 0)
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
() => routeToDisplay.value.matched[depth]
)
provide(viewDepthKey, depth + 1)
provide(matchedRouteKey, matchedRouteRef)
provide(routerViewLocationKey, routeToDisplay)
}
可以看到就是在RouterView 中进行了provide(matchedRouteKey, matchedRouteRef) 的,那么matchedRouteRef 是什么呢?
首先matchedRouteRef 是个计算属性,它的返回值是routeToDisplay.value.matched[depth] 。接着我们看routeToDisplay 和depth ,先看routeToDisplay ,routeToDisplay 也是个计算属性,它的值是props.route 或injectedRoute.value ,因为props.route 使用户传递的,所以这里我们只看injectedRoute.value ,injectedRoute 也是通过inject 获取的,获取的key是routerViewLocationKey 。看到这个key 是不是有点熟悉,在vue-router 进行install 中向app 中注入了几个变量,其中就有routerViewLocationKey 。
install(app) {
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
}
现在我们知道routeToDisplay 是当前路由的标准化对象。接下来看depth 是什么。depth 也是通过inject(viewDepthKey) 的方式获取的,但它有默认值,默认是0。你会发现紧跟着有一行provide(viewDepthKey, depth + 1) ,RouterView 又把viewDepthKey 注入进去了,不过这次值加了1。为什么这么做呢?
我们知道RouterView 是允许嵌套的,来看下面代码:
<RouterView>
<RouterView>
<RouterView />
</RouterView>
</RouterView>
在第一层RouterView 中,因为找不到对应的viewDepthKey ,所以depth 是0,然后将viewDepthKey 注入进去,并+1;在第二层中,我们可以找到viewDepthKey (在第一次中注入),depth 为1,然后再将viewDepthKey 注入,并+1,此时viewDepthKey 的值会覆盖第一层的注入;在第三层中,我们也可以找到viewDepthKey (在二层中注入,并覆盖了第一层的值),此时depth 为2。是不是发现了什么?depth 其实代表当前RouterView 在嵌套RouterView 中的深度(从0开始)。
现在我们知道了routeToDisplay 和depth ,现在我们看routeToDisplay.value.matched[depth] 。我们知道routeToDisplay.value.matched 中存储的是当前路由所匹配到的路由,并且他的顺序是父路由在子路由前。那么索引为depth 的路由有什么特别含义呢?我们看下面一个例子:
const router = createRouter({
routes: {
path: '/parent',
component: Parent,
name: 'Parent',
children: [
{
path: 'child',
name: 'Child',
component: Child,
children: [
{
name: 'ChildChild',
path: 'childchild',
component: ChildChild,
},
],
},
],
}
})
<template>
<div>
<p>parent</p>
<router-view></router-view>
</div>
</template>
<template>
<div>
<p>child</p>
<router-view></router-view>
</div>
</template>
<template>
<div>
<p>childchild</p>
</div>
</template>
使用router.resolve({ name: 'ChildChild' }) ,打印其结果,观察matched 属性。
- 在第一层
RouterView 中,depth 为0,matched[0] 为{path:'/parent', name: 'Parent', ...} (此处只列几个关键属性),level为1 - 在第二层
RouterView 中,depth 为1,matched[1] 为{path:'/parent/child', name: 'Child', ...} ,level为2 - 在第三层
RouterView 中,depth 为2,matched[2] 为{path:'/parent/child/childchild', name: 'ChildChild', ...} ,level为3
通过观察,depth 的值与路由的匹配顺序刚好一致。matched[depth].name 恰好与当前resolve 的name 一致。也就是说onBeforeRouteLeave 中的activeRecord 当前组件所匹配到的路由。
接下来看下钩子时如何注册的?在onBeforeRouteLeave ,会调用一个registerGuard 函数,registerGuard 接收三个参数:record (所在组件所匹配到的标准化路由)、name (钩子名,只能取leaveGuards 、updateGuards 之一)、guard (待添加的导航守卫)
function registerGuard(
record: RouteRecordNormalized,
name: 'leaveGuards' | 'updateGuards',
guard: NavigationGuard
) {
const removeFromList = () => {
record[name].delete(guard)
}
onUnmounted(removeFromList)
onDeactivated(removeFromList)
onActivated(() => {
record[name].add(guard)
})
record[name].add(guard)
}
onBeforeRouteUpdate
onBeforeRouteUpdate 的实现与onBeforeRouteLeave 的实现完全一致,只是调用registerGuard 传递的参数不一样。
export function onBeforeRouteUpdate(updateGuard: NavigationGuard) {
if (__DEV__ && !getCurrentInstance()) {
warn(
'getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function'
)
return
}
const activeRecord: RouteRecordNormalized | undefined = inject(
matchedRouteKey,
{} as any
).value
if (!activeRecord) {
__DEV__ &&
warn(
'No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside of a component child of <router-view>. Maybe you called it inside of App.vue?'
)
return
}
registerGuard(activeRecord, 'updateGuards', updateGuard)
}
|