【vue-rouer源码】系列文章
- 【vue-router源码】十一、onBeforeRouteLeave、onBeforeRouteUpdate源码分析
目录
前言
【源码】专注app软件定制开发系列文章将带你从0开始了解vue-router专注app软件定制开发的具体实现。专注app软件定制开发该系列文章源码参考vue-router v4.0.15。
源码地址:
专注app软件定制开发阅读该文章的前提是你最好了解vue-router专注app软件定制开发的基本使用,如果你没有使用过的话,可通过学习下。
该篇文章将分析onBeforeRouteLeave、onBeforeRouteUpdate的实现。
使用
onBeforeRouteLeave、onBeforeRouteUpdate是vue-router提供的两个composition api,它们只能被用于setup中。
export default { setup() { onBeforeRouteLeave() {} onBeforeRouteUpdate() {} }}- 1
- 2
- 3
- 4
- 5
- 6
- 7
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)}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
因为onBeforeRouteLeave是作用在组件上的,所以onBeforeRouteLeave开头就需要检查当前是否有vue实例(只在开发环境下),如果没有实例进行提示并return。
if (__DEV__ && !getCurrentInstance()) { warn( 'getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function' ) return}- 1
- 2
- 3
- 4
- 5
- 6
然后使用inject获取一个matchedRouteKey,并赋给一个activeRecord,那么个activeRecord是个什么呢?
const activeRecord: RouteRecordNormalized | undefined = inject( matchedRouteKey, // to avoid warning {} as any).value- 1
- 2
- 3
- 4
- 5
要想知道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) // ...}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
可以看到就是在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) //...}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
现在我们知道routeToDisplay是当前路由的标准化对象。接下来看depth是什么。depth也是通过inject(viewDepthKey)的方式获取的,但它有默认值,默认是0。你会发现紧跟着有一行provide(viewDepthKey, depth + 1),RouterView又把viewDepthKey注入进去了,不过这次值加了1。为什么这么做呢?
我们知道RouterView是允许嵌套的,来看下面代码:
<RouterView> <RouterView> <RouterView /> </RouterView></RouterView>- 1
- 2
- 3
- 4
- 5
在第一层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, }, ], }, ], }})- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
<!-- 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>- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
使用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)}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
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)}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24