前言

【vue-router源码】系列文章将带你从0开始理解vue-router的具体实现。该系列文章源码参考vue-router v4.0.15
源码地址:https://github.com/vuejs/router
浏览该文章的前提是你最好理解vue-router的根本应用,如果你没有应用过的话,可通过vue-router官网学习下。

该篇文章将剖析onBeforeRouteLeaveonBeforeRouteUpdate的实现。

应用

onBeforeRouteLeaveonBeforeRouteUpdatevue-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]。接着咱们看routeToDisplaydepth,先看routeToDisplayrouteToDisplay也是个计算属性,它的值是props.routeinjectedRoute.value,因为props.route使用户传递的,所以这里咱们只看injectedRoute.valueinjectedRoute也是通过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开始)。

当初咱们晓得了routeToDisplaydepth,当初咱们看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属性。

  1. 在第一层RouterView中,depth为0,matched[0]{path:'/parent', name: 'Parent', ...}(此处只列几个要害属性),level为1
  2. 在第二层RouterView中,depth为1,matched[1]{path:'/parent/child', name: 'Child', ...},level为2
  3. 在第三层RouterView中,depth为2,matched[2]{path:'/parent/child/childchild', name: 'ChildChild', ...},level为3

通过观察,depth的值与路由的匹配程序刚好统一。matched[depth].name恰好与以后resolvename统一。也就是说onBeforeRouteLeave中的activeRecord以后组件所匹配到的路由。

接下来看下钩子时如何注册的?在onBeforeRouteLeave,会调用一个registerGuard函数,registerGuard接管三个参数:record(所在组件所匹配到的标准化路由)、name(钩子名,只能取leaveGuardsupdateGuards之一)、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)}