乐趣区

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

退出移动版