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