关于前端:vuerouter源码十四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来确定须要展现的组件。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理