网络日志

【vue-router源码】十四、RouterView源码分析

前言

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

该篇文章将分析RouterView组件的实现。

使用

<RouterView></RouterView>

RouterView

export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterView',
  inheritAttrs: false,
  props: {
    // 如果设置了name,渲染对应路由配置下中components下的相应组件
    name: {
      type: String as PropType<string>,
      default: 'default',
    },
    route: Object as PropType<RouteLocationNormalizedLoaded>,
  },

  // 为@vue/compat提供更好的兼容性
  // https://github.com/vuejs/router/issues/1315
  compatConfig: { MODE: 3 },

  setup(props, { attrs, slots }) {
    // 如果<router-view>的父节点是<keep-alive>或<transition>进行提示
    __DEV__ && warnDeprecatedUsage()

    // 当前路由
    const injectedRoute = inject(routerViewLocationKey)!
    // 要展示的路由,优先取props.route
    const routeToDisplay = computed(() => props.route || injectedRoute.value)
    // router-view的深度,从0开始
    const depth = inject(viewDepthKey, 0)
    // 要展示的路由匹配到的路由
    const matchedRouteRef = computed<RouteLocationMatched | undefined>(
      () => routeToDisplay.value.matched[depth]
    )

    provide(viewDepthKey, depth + 1)
    provide(matchedRouteKey, matchedRouteRef)
    provide(routerViewLocationKey, routeToDisplay)

    const viewRef = ref<ComponentPublicInstance>()
    
    watch(
      () => [viewRef.value, matchedRouteRef.value, props.name] as const,
      ([instance, to, name], [oldInstance, from, oldName]) => {
        if (to) {
          // 当导航到一个新的路由,更新组件实例
          to.instances[name] = instance
          // 组件实例被应用于不同路由
          if (from && from !== to && instance && instance === oldInstance) {
            if (!to.leaveGuards.size) {
              to.leaveGuards = from.leaveGuards
            }
            if (!to.updateGuards.size) {
              to.updateGuards = from.updateGuards
            }
          }
        }

        // 触发beforeRouteEnter next回调
        if (
          instance &&
          to &&
          (!from || !isSameRouteRecord(to, from) || !oldInstance)
        ) {
          ;(to.enterCallbacks[name] || []).forEach(callback =>
            callback(instance)
          )
        }
      },
      { flush: 'post' }
    )

    return () => {
      const route = routeToDisplay.value
      const matchedRoute = matchedRouteRef.value
      // 需要显示的组件
      const ViewComponent = matchedRoute && matchedRoute.components[props.name]
      const currentName = props.name

      // 如果找不到对应组件,使用默认的插槽
      if (!ViewComponent) {
        return normalizeSlot(slots.default, { Component: ViewComponent, route })
      }

      // 路由中的定义的props
      const routePropsOption = matchedRoute!.props[props.name]
      // 如果routePropsOption为空,取null
      // 如果routePropsOption为true,取route.params
      // 如果routePropsOption是函数,取函数返回值
      // 其他情况取routePropsOption
      const routeProps = routePropsOption
        ? routePropsOption === true
          ? route.params
          : typeof routePropsOption === 'function'
          ? routePropsOption(route)
          : routePropsOption
        : null

      // 当组件实例被卸载时,删除组件实例以防止泄露
      const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
        if (vnode.component!.isUnmounted) {
          matchedRoute!.instances[currentName] = null
        }
      }

      // 生成组件
      const component = h(
        ViewComponent,
        assign({}, routeProps, attrs, {
          onVnodeUnmounted,
          ref: viewRef,
        })
      )

      if (
        (__DEV__ || __FEATURE_PROD_DEVTOOLS__) &&
        isBrowser &&
        component.ref
      ) {
        // ...
      }

      return (
        // 有默认插槽则使用默认默认插槽,否则直接使用component
        normalizeSlot(slots.default, { Component: component, route }) ||
        component
      )
    }
  },
})

为了更好理解router-view的渲染过程,我们看下面的例子:

先规定我们的路由表如下:

const router = createRouter({
  // ...
  // Home和Parent都是两个简单组件
  routes: [
    {
      name: 'Home',
      path: '/',
      component: Home,
    },
    {
      name: 'Parent',
      path: '/parent',
      component: Parent,
    },
  ]
})

假设我们的地址是http://localhost:3000。现在我们访问http://localhost:3000,你肯定能够想到router-view中显示的肯定是Home组件。那么它是怎样渲染出来的呢?

首先我们要知道vue-router在进行install时,会进行第一次的路由跳转并立马向app注入一个默认的currentRouteSTART_LOCATION_NORMALIZED),此时router-view会根据这个currentRoute进行第一次渲染。因为这个默认的currentRoute中的matched是空的,所以第一次渲染的结果是空的。等到第一次路由跳转完毕后,会执行一个finalizeNavigation方法,在这个方法中更新currentRoute,这时在currentRoute中就可以找到需要渲染的组件Homerouter-view完成第二次渲染。第二次完成渲染后,紧接着触发router-view中的watch,将最新的组件实例赋给to.instance[name],并循环执行to.enterCallbacks[name](通过在钩子中使用next()添加的函数,过程结束。

然后我们从http://localhost:3000跳转至http://localhost:3000/parent,假设使用push进行跳转,同样在跳转完成后会执行finalizeNavigation,更新currentRoute,这时router-view监听到currentRoute的变化,找到需要渲染的组件,将其显示。在渲染前先执行旧组件卸载钩子,将路由对应的instance重置为null。渲染完成后,接着触发watch,将最新的组件实例赋给to.instance[name],并循环执行to.enterCallbacks[name],过程结束。

在之前分析router.push的过程中,我们曾经得到过一个欠完整的导航解析流程,那么在这里我们可以将其补齐了:

  1. 导航被触发
  2. 调用失活组件中的beforeRouteLeave钩子
  3. 调用全局beforeEach钩子
  4. 调用重用组件内的beforeRouteUpdate钩子
  5. 调用路由配置中的beforeEnter钩子
  6. 解析异步路由组件
  7. 调用激活组件中的beforeRouteEnter钩子
  8. 调用全局的beforeResolve钩子
  9. 导航被确认
  10. 调用全局的afterEach钩子
  11. DOM更新
  12. 调用beforeRouteEnter守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

总结

router-view根据currentRoutedepth找到匹配到的路由,然后根据props.nameslots.default来确定需要展示的组件。