共计 5407 个字符,预计需要花费 14 分钟才能阅读完成。
前言
【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) {
// 开发模式下没有组件实例,进行提醒并 return
if (__DEV__ && !getCurrentInstance()) {
warn('getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function')
return
}
// matchedRouteKey 是在 RouterView 中进行 provide 的,示意以后组件所匹配到到的路由记录(通过标准化解决的)const activeRecord: RouteRecordNormalized | undefined = inject(
matchedRouteKey,
// to avoid warning
{} 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,
// to avoid warning
{} 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))
// currentRoute 路由标准化对象
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,
},
],
},
],
}
})
<!-- Parent -->
<template>
<div>
<p>parent</p>
<router-view></router-view>
</div>
</template>
<!-- Child -->
<template>
<div>
<p>child</p>
<router-view></router-view>
</div>
</template>
<!-- ChildChild -->
<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)
// 被 keep-alive 缓存的组件失活时移除钩子
onDeactivated(removeFromList)
// 被 keep-alive 缓存的组件激活时增加钩子
onActivated(() => {record[name].add(guard)
})
// 增加钩子,record[name]是个 set,在路由标准化时解决的
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,
// to avoid warning
{} 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)
}