前言

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