【vue-router源码】十四、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
注入一个默认的currentRoute
(START_LOCATION_NORMALIZED
),此时router-view
会根据这个currentRoute
进行第一次渲染。因为这个默认的currentRoute
中的matched
是空的,所以第一次渲染的结果是空的。等到第一次路由跳转完毕后,会执行一个finalizeNavigation
方法,在这个方法中更新currentRoute
,这时在currentRoute
中就可以找到需要渲染的组件Home
,router-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
的过程中,我们曾经得到过一个欠完整的导航解析流程,那么在这里我们可以将其补齐了:
- 导航被触发
- 调用失活组件中的
beforeRouteLeave
钩子 - 调用全局
beforeEach
钩子 - 调用重用组件内的
beforeRouteUpdate
钩子 - 调用路由配置中的
beforeEnter
钩子 - 解析异步路由组件
- 调用激活组件中的
beforeRouteEnter
钩子 - 调用全局的
beforeResolve
钩子 - 导航被确认
- 调用全局的
afterEach
钩子 - DOM更新
- 调用
beforeRouteEnter
守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
总结
router-view
根据currentRoute
及depth
找到匹配到的路由,然后根据props.name
、slots.default
来确定需要展示的组件。