【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来确定需要展示的组件。
转载自:https://segmentfault.com/a/1190000041990345