美文网首页【vue-rouer源码】
【vue-router源码】十四、RouterView源码分析

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

作者: MAXLZ | 来源:发表于2022-06-15 22:45 被阅读0次

    前言

    【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来确定需要展示的组件。

    相关文章

      网友评论

        本文标题:【vue-router源码】十四、RouterView源码分析

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