美文网首页【vue-rouer源码】
【vue-router源码】十一、onBeforeRouteLe

【vue-router源码】十一、onBeforeRouteLe

作者: MAXLZ | 来源:发表于2022-06-12 21:56 被阅读0次

    前言

    【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)
    }
    

    相关文章

      网友评论

        本文标题:【vue-router源码】十一、onBeforeRouteLe

        本文链接:https://www.haomeiwen.com/subject/nmjnmrtx.html