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