Vue-Router源码分析(三): RouterView渲染过程

js 复制代码
<template>
  <div>
    <router-link to="/">Go to Home</router-link>
    <router-link to="/about">Go to About</router-link>
  </div>

  <router-view></router-view>
</template>
js 复制代码
import { createRouter, createWebHistory } from 'vue-router'

import Main from './views/Main.vue'
import My from './views/My.vue'

export const routerHistory = createWebHistory()
export const router = createRouter({
  history: routerHistory,
  strict: true,
  routes: [
    {
      path: '/',
      component: Main,
    },
    { path: '/about', component: My },
  ],
})

上面这段代码是 Vue Router 的最基本使用方式,上一篇介绍过,router-viewrouter-link组件能在全局使用,是因为在Vue Router 初始化过程中,全局根组件app,已经注册了:

js 复制代码
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)

每当用户切换路由,router-view总是能知道自己渲染的是哪个组件。其实思路比较简单: 拿到当前的route信息,每个route信息都会包含component组件信息,渲染这个component组件就行。

具体逻辑:

方法一:

一:在源码中:createRouter方法内部维护了一个 currentRoute 浅层响应式属性

js 复制代码
 const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
    START_LOCATION_NORMALIZED
  )

并给出初始值

js 复制代码
export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = {
  path: '/',
  name: undefined,
  params: {},
  query: {},
  hash: '',
  fullPath: '/',
  matched: [],
  meta: {},
  redirectedFrom: undefined,
}

一般情况下,我们都会以 路径: "/" 来表示首页

二:在router 初始化(install)过程中,跟组件app会将 currentRoute 信息 透传到 子孙组件

js 复制代码
app.provide(routerViewLocationKey, currentRoute)

接下来 来看router-view 组件

js 复制代码
export const RouterViewImpl = defineComponent({
  name: 'RouterView',
  inheritAttrs: false,
  props: {
      /** 代码 省略*/
  },

  setup(props, { attrs, slots }) {
     /** 代码 省略*/
  },
})

这么一看,这不就是咱们在业务中的组件写法么。

三: 来到setup方法中,获取跟组件app提供的currentRoute 信息,将injectedRoute包装成计算属性,这样每次根组件app注入进来的injectedRoute发生变化时,router-view就能获取到最新值。

js 复制代码
  const injectedRoute = inject(routerViewLocationKey)!
  const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
    () => props.route || injectedRoute.value
  )

切换路由时,打印的相关路由信息

四:接下来要获取matched 数组内容,这里面才有route对应的要渲染的组件信息

js 复制代码
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
   () => routeToDisplay.value.matched[depth.value]
)

这里有一个depth属性,这里先暂不介绍,放到后面再说,你可以默认为0。这样就能拿到匹配的组件信息,matchedRouteRef,打印下看看都有啥

可以看到有路由信息:path、meta、redirect等,也有一些组件专属的路由钩子函数,这些钩子函数只会负责该组件,全局的路由钩子跟这个负责的对象不一样,不能搞混。

这样就拿到components属性了,就可以渲染组件了。

js 复制代码
  const currentName = props.name

  // 获取匹配到的路由信息
  const matchedRoute = matchedRouteRef.value
  
  // 获取要渲染的组件
  const ViewComponent =
    matchedRoute && matchedRoute.components![currentName]

 // 如果没找到对应的name,构建一个默认插槽
  if (!ViewComponent) {
    return normalizeSlot(slots.default, { Component: ViewComponent, route })
  }

对于每个router-view都有一个默认的name属性,默认值:default。

js 复制代码
  props: {
    name: {
      type: String as PropType<string>,
      default: 'default',
    },
    route: Object as PropType<RouteLocationNormalizedLoaded>,
  },

五:如果你在页面中,搞了多个router-view,这个属性的关键作用就是区分是哪个router-view。详见官网:(命名视图 | Vue Router (vuejs.org))

这里便于理解,默认就是default

当没有找到对应name 的 router-view, 就会给一个默认的插槽组件,插槽内容就是router-view 包裹的子组件,并将 当前的路由信息route 当作插槽属性抛出去。

js 复制代码
<router-view v-slot="{ Component, route }"> 
    <component :is="Component" ref="mainContent" /> 
</router-view>

六:正常情况下,都是有ViewComponent的。接下来解析路由参数,路由参数支持3种写法:

  1. 布尔模式
  2. 对象模式
  3. 函数模式

分别对应下面代码:

js 复制代码
const routes = [{ path: '/user/:id', component: User, props: true }]
js 复制代码
const routes = [ { path: '/user/:id', components: { default: User, sidebar: Sidebar }, props: { default: true, sidebar: false } } ]
js 复制代码
const routes = [ { path: '/search', component: SearchUser, props: route => ({ query: route.query.q }) } ]

源码中处理路由参数:

js 复制代码
const routePropsOption = matchedRoute.props[currentName]
const routeProps = routePropsOption
? routePropsOption === true
  ? route.params
  : typeof routePropsOption === 'function'
  ? routePropsOption(route)
  : routePropsOption
: null
  1. 如果是布尔模式,直接把params参数当作路由参数
  2. 如果是函数,调用函数
  3. 如果是对象,直接用对象

七:处理组件生命周期钩子函数:

js 复制代码
 const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
     // vnode中挂在的组件被销毁后,这里重置路由上的组件实例
    if (vnode.component!.isUnmounted) {
      matchedRoute.instances[currentName] = null
    }
  }

八:包装组件和属性:

js 复制代码
 const component = h(
    ViewComponent,
    assign({}, routeProps, attrs, {
      onVnodeUnmounted,
      ref: viewRef,
    })
  )

九:渲染组件

js 复制代码
 return (
    normalizeSlot(slots.default, { Component: component, route }) ||
    component
  )

这里也是把component 和 route 抛出去

方法二

router-view上暴露出 route属性,你可以直接修改route属性

js 复制代码
<template>
  <div>
    <router-link to="/">Go to Home</router-link>
    <router-link to="/about">Go to About</router-link>
  </div>

  <router-view :route="currentRoute"></router-view>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useRoute } from 'vue-router'

export default defineComponent({
  name: 'App',
  setup() {
    const currentRoute = useRoute()
    return {
      currentRoute
    }
  },
})
</script>

回到源码中,当用户手动为router-view设置route,就以用户传入的为主:

js 复制代码
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
  () => props.route || injectedRoute.value
)

其他后续逻辑都是一样的。

关于depth属性

这里涉及到深度优先遍历,找到有components那一项配置,渲染这个组件。

业务中可能会有下面的代码:

js 复制代码
export const router = createRouter({
  history: routerHistory,
  strict: true,
  routes: [
    {
      path: '/',
      redirect: '/children',
      children: [
        {
          path: '/children',
          redirect: '/v1',
          children: [
            {
              path: '/v1',
              component: Main,
            },
          ],
        },
      ],
    },
    { path: '/about', component: My },
  ],
})

一级路由 甚至二三级路由都没有明确指定component,直到深层次的子组件才有component。拿这个例子来说, 此时depth 为2,matchedRouteRef匹配到的就是 path 为 "/v1" 这个对应的组件。

源码中匹配规则:

js 复制代码
const depth = computed<number>(() => {
  let initialDepth = unref(injectedDepth)
  const { matched } = routeToDisplay.value
  let matchedRoute: RouteLocationMatched | undefined
  while (
    (matchedRoute = matched[initialDepth]) &&
    !matchedRoute.components
  ) {
    initialDepth++
  }
  return initialDepth
})

总结

  1. router-view 会从props中获取当前匹配到的route,如果用户没有明确指定route,就会去根组件app中拿到匹配的路由
  2. 解析路由,获取对应的component,涉及到深度优先遍历。
  3. 解析路由参数和组件实例, 调用Vue内部h函数,包装成虚拟节点VNode,将其返回。
  4. 当路由变更时,重新获取对应的component组件,再次渲染。

扩展

说到 useRoute 和 useRouter这两个hooks,实现原理也很简单,由根组件app提供 provide 注入参数,hooks中inject到对应的数据。

js 复制代码
const router: Router = {

    install(app: App) {
        const router = this
        // router就是这个router大对象
        app.provide(routerKey, router)
        // currentRoute就是当前路由
        app.provide(routerViewLocationKey, currentRoute)
    }
}
js 复制代码
export function useRouter(): Router {
  return inject(routerKey)!
}
js 复制代码
export function useRoute(): RouteLocationNormalizedLoaded {
  return inject(routeLocationKey)!
}
相关推荐
栈老师不回家8 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙14 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠18 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds38 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓3 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 小时前
无网络安装ionic和运行
前端·npm