VueRouter源码分析(四)-- RouterView

338 阅读3分钟

VueRouter源码分析(四)-- RouterView

作为我学习VueRouter源码系列最后一节,也不搞什么花里胡哨的华丽退场了,还是走前篇的老路。谁不喜欢简简单单省力省心的事儿呢。

1. RouterViewProps

类型接口RouterViewProps,就nameroute俩属性。

export interface RouterViewProps {
  name?: string
  // allow looser type for user facing api
  route?: RouteLocationNormalized
}

2. RouterViewImpl

好,才刚打一个小怪,boss就来了。简要的信息也没什么,不如直接看setup方法。

export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterView',
  // #674 we manually inherit them
  inheritAttrs: false,
  props: {
    name: {
      type: String as PropType<string>,
      default: 'default',
    },
    route: Object as PropType<RouteLocationNormalizedLoaded>,
  },
​
  // Better compat for @vue/compat users
  // https://github.com/vuejs/router/issues/1315
  compatConfig: { MODE: 3 },
​
  setup(props, { attrs, slots }) {
      // ...
  }
  },
})

setup方法

一直比较好奇RouterView是如何确定要渲染的内容的。原来是维护了一个嵌套深度depth,再根据给定的路由route或当前路由currentRoute来得到matched数组,根据深度确定matchedRoute,取出其中名称对应的components,再辅以路由传参来渲染内容。

setup(props, { attrs, slots }) {
    // `dev`环境下会先检测是否是已弃用的写法,即`RouterView`现在不能直接用作`transition`或者`KeepAlive`的子组件。
    __DEV__ && warnDeprecatedUsage()
​
    // 最外层的`RouterView`是`createRouter`中提供的`currentRoute`,内部的则是对应的外层的`RouterView`提供的
    const injectedRoute = inject(routerViewLocationKey)!
    // 要展示的路由:要么是通过`props`提供的路由,要么是当前页对应的路由
    const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
      () => props.route || injectedRoute.value
    )
    // 深度默认0
    const injectedDepth = inject(viewDepthKey, 0)
    // The depth changes based on empty components option, which allows passthrough routes e.g. routes with children
    // that are used to reuse the `path` property
    const depth = computed<number>(() => {
      let initialDepth = unref(injectedDepth)
      const { matched } = routeToDisplay.value
      let matchedRoute: RouteLocationMatched | undefined
      // `matched`数组是父级路由在前,子级在后,从当前`RouterView`对应的路由深度开始,顺着`matched`数组往下找,
      // 路由有`components`则`RouterView`的深度+1,没有则深度不变,直到遍历完整个`matched`数组
      while (
        (matchedRoute = matched[initialDepth]) &&
        !matchedRoute.components
      ) {
        initialDepth++
      }
      return initialDepth
    })
    const matchedRouteRef = computed<RouteLocationMatched | undefined>(
      () => routeToDisplay.value.matched[depth.value]
    )
​
    // 提供一些数据,以便内部嵌套的`RouterView`注入继承
    provide(
      viewDepthKey,
      computed(() => depth.value + 1)
    )
    provide(matchedRouteKey, matchedRouteRef)
    provide(routerViewLocationKey, routeToDisplay)
​
    const viewRef = ref<ComponentPublicInstance>()
​
    // watch at the same time the component instance, the route record we are
    // rendering, and the name
    // 侦听组件实例、路由以及路由名称
    watch(
      () => [viewRef.value, matchedRouteRef.value, props.name] as const,
      ([instance, to, name], [oldInstance, from, oldName]) => {
        // copy reused instances
        if (to) {
          // this will update the instance for new instances as well as reused
          // instances when navigating to a new route
          to.instances[name] = instance
          // the component instance is reused for a different route or name, so
          // we copy any saved update or leave guards. With async setup, the
          // mounting component will mount before the matchedRoute changes,
          // making instance === oldInstance, so we check if guards have been
          // added before. This works because we remove guards when
          // unmounting/deactivating components
          // 复用组件的路由钩子
          if (from && from !== to && instance && instance === oldInstance) {
            if (!to.leaveGuards.size) {
              to.leaveGuards = from.leaveGuards
            }
            if (!to.updateGuards.size) {
              to.updateGuards = from.updateGuards
            }
          }
        }
​
        // trigger beforeRouteEnter next callbacks
        // 触发`beforeRouteEnter`的`next`回调
        if (
          instance &&
          to &&
          // if there is no instance but to and from are the same this might be
          // the first visit
          (!from || !isSameRouteRecord(to, from) || !oldInstance)
        ) {
          ;(to.enterCallbacks[name] || []).forEach(callback =>
            callback(instance)
          )
        }
      },
      { flush: 'post' }
    )
​
    return () => {
      const route = routeToDisplay.value
      // we need the value at the time we render because when we unmount, we
      // navigated to a different location so the value is different
      const currentName = props.name
      const matchedRoute = matchedRouteRef.value
      const ViewComponent =
        matchedRoute && matchedRoute.components![currentName]
​
      if (!ViewComponent) {
        return normalizeSlot(slots.default, { Component: ViewComponent, route })
      }
​
      // props from route configuration
      // 拿到路由传参以辅助渲染
      const routePropsOption = matchedRoute.props[currentName]
      const routeProps = routePropsOption
        ? routePropsOption === true
          ? route.params
          : typeof routePropsOption === 'function'
          ? routePropsOption(route)
          : routePropsOption
        : null
​
      // 虚拟节点卸载时移除对应实例
      const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
        // remove the instance reference to prevent leak
        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
      ) {
        // TODO: can display if it's an alias, its props
        const info: RouterViewDevtoolsContext = {
          depth: depth.value,
          name: matchedRoute.name,
          path: matchedRoute.path,
          meta: matchedRoute.meta,
        }
​
        const internalInstances = isArray(component.ref)
          ? component.ref.map(r => r.i)
          : [component.ref.i]
​
        internalInstances.forEach(instance => {
          // @ts-expect-error
          instance.__vrv_devtools = info
        })
      }
​
      // 要渲染的组件
      return (
        // pass the vnode to the slot as a prop.
        // h and <component :is="..."> both accept vnodes
        normalizeSlot(slots.default, { Component: component, route }) ||
        component
      )
    }

这些简短的内容阅读起来令人愉快,VueRouter也打算在这儿结束了。花了好几天来写,真的挺费手的。下辈子一定要多长几只手。