VueRouter源码分析(四)-- RouterView
作为我学习VueRouter源码系列最后一节,也不搞什么花里胡哨的华丽退场了,还是走前篇的老路。谁不喜欢简简单单省力省心的事儿呢。
1. RouterViewProps
类型接口RouterViewProps,就name和route俩属性。
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也打算在这儿结束了。花了好几天来写,真的挺费手的。下辈子一定要多长几只手。