剖析 Vue Router 源码中 `RouterView` 和 `RouterLink` 组件的实现,它们如何与路由实例交互。

大家好!路由探索之旅,现在发车!

今天咱们不开车,聊聊Vue Router的两个重要组件:RouterViewRouterLink。 这俩哥们儿,一个负责展示,一个负责跳转,在Vue单页面应用里扮演着举足轻重的角色。 咱们一起扒一扒它们的源码,看看它们是如何跟路由实例眉来眼去的。

一、RouterView:路由的“展示窗口”

想象一下,你家客厅的电视机,RouterView 就相当于这个电视机。 路由配置决定了播放哪个频道(组件),RouterView 就负责把这个频道(组件)的内容渲染出来。

1.1 RouterView 的核心逻辑

RouterView 的主要职责是:

  • 响应路由变化: 监听路由实例的 currentRoute 对象,一旦发生变化,就重新渲染。
  • 动态渲染组件: 根据 currentRoute 对象中的 matched 数组,找到匹配的组件,并进行渲染。
  • 处理嵌套路由: 支持多层嵌套的路由,每个 RouterView 负责渲染当前层级的组件。

1.2 RouterView 的源码剖析 (简化版)

为了便于理解,我们来看一个简化版的 RouterView 组件的实现:

// RouterView.js (简化版)
import { h, defineComponent, inject, computed } from 'vue';
import { RouterContext } from './injectionSymbols';

export default defineComponent({
  name: 'RouterView',
  props: {
    name: {
      type: String,
      default: 'default',
    },
  },
  setup(props, { attrs, slots }) {
    const router = inject(RouterContext); // 从父组件注入 router 实例
    const route = computed(() => router.currentRoute.value); // 获取响应式的 currentRoute

    const depth = inject('routerViewDepth', 0); // 获取当前 RouterView 的嵌套深度
    provide('routerViewDepth', depth + 1); // 向子组件提供嵌套深度

    const matchedRouteRef = computed(() => {
      const matched = route.value.matched;
      return matched[depth]; // 获取当前深度的 matched 路由记录
    });

    return () => {
      const matchedRoute = matchedRouteRef.value;

      // 如果没有匹配的路由记录,则不渲染任何内容
      if (!matchedRoute) {
        return slots.default ? slots.default() : null;
      }

      const component = matchedRoute.components[props.name]; // 获取组件

      // 如果没有组件,则不渲染任何内容
      if (!component) {
        return h('div', 'No component found for ' + props.name);
      }

      // 传递 props 和 attrs 给组件
      const routeProps = matchedRoute.props[props.name];
      const propsData = routeProps === true ? route.value.params : (typeof routeProps === 'function' ? routeProps(route.value) : routeProps);

      const allProps = {
        ...propsData,
        ...attrs,
      };

      // 渲染组件
      return h(component, allProps, slots.default ? slots.default() : null);
    };
  },
});

代码解释:

  1. inject(RouterContext) RouterView 通过 inject 从祖先组件 (通常是 App 组件) 注入 router 实例。 这样 RouterView 才能访问路由信息,比如 currentRouteRouterContext 是一个 Symbol,用于在组件之间传递 router 实例。
  2. computed(() => router.currentRoute.value) currentRoute 是一个响应式对象,包含了当前路由的信息。 RouterView 通过 computed 创建一个响应式的 route 计算属性,以便在路由变化时自动更新。
  3. inject('routerViewDepth', 0)provide('routerViewDepth', depth + 1) 这两个是用来处理嵌套路由的关键。 routerViewDepth 记录了 RouterView 的嵌套深度。 每个 RouterView 都增加深度并传递给子组件。 这样,嵌套的 RouterView 就能知道自己应该渲染哪个路由记录。
  4. matched[depth] currentRoute.matched 是一个数组,包含了从根路由到当前路由的所有匹配的路由记录。 depth 决定了 RouterView 应该使用哪个路由记录来渲染组件。
  5. matchedRoute.components[props.name] 每个路由记录都有一个 components 对象,包含了该路由对应的组件。 props.name 允许我们在一个路由记录中指定多个具名视图。 默认情况下,name'default'
  6. h(component, allProps, slots.default ? slots.default() : null) 使用 h 函数创建一个 VNode,渲染组件。 allProps 包含了从路由记录中获取的 props 和传递给 RouterView 的 attributes。 slots.default 允许我们向组件传递插槽。

流程图:

步骤 描述 涉及到的关键变量/函数
1 从父组件注入 router 实例。 inject(RouterContext)
2 获取响应式的 currentRoute 对象。 computed(() => router.currentRoute.value)
3 获取当前 RouterView 的嵌套深度。 inject('routerViewDepth', 0)
4 向子组件提供新的嵌套深度。 provide('routerViewDepth', depth + 1)
5 currentRoute.matched 数组中找到当前深度的匹配的路由记录。 matched[depth]
6 如果没有匹配的路由记录,则不渲染任何内容。
7 从匹配的路由记录中获取要渲染的组件。 matchedRoute.components[props.name]
8 如果没有组件,则不渲染任何内容。
9 从路由记录中获取 props,并与传递给 RouterView 的 attributes 合并。 matchedRoute.props[props.name], attrs
10 使用 h 函数创建一个 VNode,渲染组件。 h(component, allProps, slots.default ? slots.default() : null)

总结:

RouterView 的核心在于响应路由变化,并根据当前路由信息动态渲染对应的组件。 通过 injectprovide 实现嵌套路由的支持。

二、RouterLink:路由的“传送门”

RouterLink 就像网页上的链接,点击它可以跳转到不同的页面。 在Vue Router 中,RouterLink 负责生成带有正确 href 属性的 <a> 标签,并阻止默认的链接跳转行为,改用 router.pushrouter.replace 来更新路由。

2.1 RouterLink 的核心逻辑

RouterLink 的主要职责是:

  • 生成链接: 根据 to 属性生成正确的 href 属性。
  • 阻止默认行为: 阻止 <a> 标签的默认跳转行为。
  • 触发路由跳转: 调用 router.pushrouter.replace 来更新路由。
  • 处理激活状态: 根据当前路由是否匹配 to 属性,添加或移除激活 class。

2.2 RouterLink 的源码剖析 (简化版)

// RouterLink.js (简化版)
import { h, defineComponent, inject, computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { RouterContext } from './injectionSymbols';
import { useRoute } from './useRoute';
import { useRouter } from './useRouter';

export default defineComponent({
  name: 'RouterLink',
  props: {
    to: {
      type: [String, Object],
      required: true,
    },
    tag: {
      type: String,
      default: 'a',
    },
    activeClass: {
      type: String,
      default: 'router-link-active',
    },
    exactActiveClass: {
      type: String,
      default: 'router-link-exact-active',
    },
    replace: {
      type: Boolean,
      default: false,
    },
    ariaCurrentValue: {
      type: String,
      default: 'page',
    },
  },
  setup(props, { slots }) {
    const router = useRouter();
    const route = useRoute();
    const isActive = ref(false);
    const isExactActive = ref(false);
    const linkRef = ref(null);

    const normalizedTo = computed(() => router.resolve(props.to));

    const updateActiveClass = () => {
      const currentRoute = route;
      isActive.value = normalizedTo.value.href === currentRoute.fullPath;
      isExactActive.value = normalizedTo.value.path === currentRoute.path;
    };

    onMounted(() => {
      updateActiveClass();
      router.afterEach(updateActiveClass);
    });

    onBeforeUnmount(() => {
      router.afterEach(updateActiveClass);
    });

    const onClick = (evt) => {
      if (props.replace) {
        router.replace(props.to);
      } else {
        router.push(props.to);
      }
      evt.preventDefault();
    };

    return () => {
      const { tag, activeClass, exactActiveClass, ariaCurrentValue } = props;
      const active = isActive.value;
      const exactActive = isExactActive.value;

      const linkClass = {
        [activeClass]: active,
        [exactActiveClass]: exactActive,
      };

      const attrs = {
        href: normalizedTo.value.href,
        onClick,
      };

      if (tag === 'a' && ariaCurrentValue && exactActive) {
        attrs['aria-current'] = ariaCurrentValue;
      }

      return h(
        tag,
        {
          ...attrs,
          class: linkClass,
          ref: linkRef,
        },
        slots.default ? slots.default() : null
      );
    };
  },
});

代码解释:

  1. useRouter()useRoute() RouterLink 使用 useRouteruseRoute hooks 来访问 router 实例和 route 对象。
  2. normalizedTo = computed(() => router.resolve(props.to)) router.resolve(props.to)to 属性转换为一个标准化的路由对象,包含了 hrefpathquery 等信息。 normalizedTo 是一个计算属性,以便在 to 属性变化时自动更新。
  3. updateActiveClass() 这个函数负责更新激活 class。 它比较 normalizedTo.hrefcurrentRoute.fullPath 来判断是否激活,并设置 isActive.valueisExactActive.value
  4. onMounted(() => { ... }) 在组件挂载后,调用 updateActiveClass() 更新激活 class,并使用 router.afterEach(updateActiveClass) 注册一个全局后置钩子,在每次路由切换后都更新激活 class。
  5. onClick(evt) 这个函数处理点击事件。 它调用 router.pushrouter.replace 来更新路由,并使用 evt.preventDefault() 阻止默认的链接跳转行为。
  6. h(tag, { ...attrs, class: linkClass, ref: linkRef }, slots.default ? slots.default() : null) 使用 h 函数创建一个 VNode,渲染链接。 attrs 包含了 hrefonClick 等属性。 linkClass 包含了激活 class。 slots.default 允许我们向链接传递插槽。

流程图:

步骤 描述 涉及到的关键变量/函数
1 使用 useRouter()useRoute() 获取 router 实例和 route 对象。 useRouter(), useRoute()
2 使用 router.resolve(props.to)to 属性转换为标准化的路由对象。 router.resolve(props.to)
3 创建 updateActiveClass() 函数,用于更新激活 class。
4 在组件挂载后,调用 updateActiveClass() 更新激活 class,并使用 router.afterEach(updateActiveClass) 注册一个全局后置钩子。 onMounted(), router.afterEach(updateActiveClass)
5 创建 onClick(evt) 函数,处理点击事件,调用 router.pushrouter.replace 更新路由,并阻止默认的链接跳转行为。 onClick(evt), router.push(props.to), router.replace(props.to), evt.preventDefault()
6 使用 h 函数创建一个 VNode,渲染链接,包含 href 属性,onClick 事件,以及激活 class。 h(tag, { ...attrs, class: linkClass, ref: linkRef }, slots.default ? slots.default() : null)

总结:

RouterLink 的核心在于生成链接,阻止默认行为,并触发路由跳转。 它还负责处理激活状态,以便在当前路由匹配 to 属性时添加激活 class。

三、RouterView 和 RouterLink 如何与路由实例交互

现在我们已经了解了 RouterViewRouterLink 的基本实现,让我们看看它们是如何与路由实例交互的。

  • RouterView:

    • 依赖注入: 通过 inject(RouterContext) 从祖先组件注入 router 实例。
    • 响应式数据: 通过 computed(() => router.currentRoute.value) 获取响应式的 currentRoute 对象。
    • 嵌套路由: 通过 inject('routerViewDepth', 0)provide('routerViewDepth', depth + 1) 处理嵌套路由。
  • RouterLink:

    • 访问路由实例: 使用 useRouter() hook 来访问 router 实例。
    • 访问路由信息: 使用 useRoute() hook 来访问 route 对象。
    • 生成链接: 使用 router.resolve(props.to)to 属性转换为标准化的路由对象。
    • 触发跳转: 使用 router.pushrouter.replace 来更新路由。
    • 监听路由变化: 通过 router.afterEach注册钩子,检测路由变化。

交互图:

+-----------------+      inject(RouterContext)     +-----------------+
|     RouterView    | <---------------------------- |     Router      |
+-----------------+                               +-----------------+
       |                                               ^
       |  computed(() => router.currentRoute.value)      |
       |                                               |
       v                                               |
+-----------------+                               |  router.push()  |
|   CurrentRoute    |                               |  router.replace() |
+-----------------+                               v
                                        +-----------------+
                                        |    RouterLink   |
                                        +-----------------+
                                               ^   |
                                               |   | useRouter(), useRoute()
                                               |   v
                                        +-----------------+
                                        |       Route     |
                                        +-----------------+

表格对比:

特性 RouterView RouterLink
主要功能 展示组件 导航跳转
依赖的路由实例 通过 inject(RouterContext) 获取 通过 useRouter() 获取
响应式数据 computed(() => router.currentRoute.value) useRoute() 返回的 route 对象
路由跳转 router.pushrouter.replace
嵌套路由 支持 无需关心嵌套路由,路由实例负责处理
激活状态 根据当前路由是否匹配 to 属性添加/移除激活 class

四、总结与展望

今天我们一起探索了 Vue Router 中 RouterViewRouterLink 组件的实现,了解了它们如何与路由实例交互。 RouterView 负责展示组件,RouterLink 负责导航跳转,它们是构建单页面应用的重要基石。

当然,我们今天看的只是简化版的源码,实际的源码要复杂得多。 但是,理解了这些核心概念,就能更好地使用 Vue Router,并能更深入地理解其内部机制。

希望这次旅程能帮助大家更好地掌握 Vue Router,写出更优雅、更高效的 Vue 应用! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注