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

各位靓仔靓女,今天咱们来聊聊Vue Router里的两个重要角色:RouterViewRouterLink。别害怕,虽然听起来高大上,但拆开了看,其实就是两个平易近人的组件。咱们要做的,就是扒开它们的外衣,看看它们是怎么跟 Vue Router 这个“老大哥”眉来眼去的。

开场白:RouterView 和 RouterLink 的职责

简单来说:

  • RouterView:一个“占位符”,告诉 Vue:“嘿,兄弟,这里要显示匹配当前路由的组件了!”
  • RouterLink:一个“导航员”,负责生成链接,点击它就能触发路由切换。

说白了,一个负责显示内容,一个负责切换内容。

第一幕:RouterView 的“占位艺术”

RouterView 的核心任务是根据当前路由,动态渲染对应的组件。它是怎么做到的呢?咱们先来简化一下 RouterView 的源码,看看它的骨架:

// 简化版 RouterView
const RouterView = {
  name: 'RouterView',
  functional: true, // 函数式组件,性能更棒
  props: {
    name: {
      type: String,
      default: 'default' // 命名视图
    }
  },
  render(h, { parent, data, props }) {
    let route = parent.$route; // 获取当前路由对象
    let matched = route.matched; // 获取匹配的路由记录数组

    data.routerView = true; // 标记这是一个 RouterView 组件

    let depth = 0;
    let parentRouteView = parent;
    while (parentRouteView && parentRouteView.$vnode && parentRouteView.$vnode.data) {
      if (parentRouteView.$vnode.data.routerView) {
        depth++; // 计算 RouterView 的嵌套深度
      }
      parentRouteView = parentRouteView.$parent;
    }

    let record = matched[depth]; // 获取当前 RouterView 应该渲染的路由记录
    if (!record) {
      return h(); // 没有匹配的路由,渲染一个空 VNode
    }

    let component = record.components[props.name]; // 获取组件
    if (!component) {
      return h(); // 组件不存在,渲染一个空 VNode
    }

    const renderProps = { ...route.params, ...route.query }; // 合并参数

    // 返回 VNode
    return h(component, data, renderProps);
  }
};

别被代码吓到,咱们一行行来解释:

  1. functional: trueRouterView 是一个函数式组件,这意味着它没有自己的状态,渲染性能更好。
  2. props: { name: ... }name 属性用于支持命名视图,允许多个 RouterView 同时显示不同的组件。
  3. render(h, { parent, data, props })render 函数是函数式组件的核心,它负责生成 VNode。
    • parent.$route:获取当前路由对象。Vue Router 会把路由对象注入到每个组件的 vm.$route 属性中。
    • route.matched:获取匹配的路由记录数组。路由记录包含了路由的配置信息,例如组件、路径等。
    • depth:计算 RouterView 的嵌套深度。这个深度决定了哪个路由记录应该被渲染。比如,如果 RouterView 嵌套了两层,那么它应该渲染 route.matched[2] 对应的组件。
    • record = matched[depth]:根据深度获取对应的路由记录。
    • component = record.components[props.name]:从路由记录中获取组件。这里使用了 props.name 来支持命名视图。
    • h(component, data, renderProps):使用 h 函数创建 VNode。component 是要渲染的组件,data 包含了 VNode 的属性,renderProps 包含了要传递给组件的 props。

关键点:路由匹配和嵌套深度

RouterView 的核心逻辑在于如何确定哪个路由记录应该被渲染。这涉及到两个关键概念:

  • 路由匹配:Vue Router 会根据当前 URL,找到匹配的路由记录。匹配的路由记录会被存储在 route.matched 数组中。
  • 嵌套深度RouterView 可以嵌套在其他组件中,因此需要计算它的嵌套深度,以确定它应该渲染 route.matched 数组中的哪个元素。

举个例子:

// 路由配置
const routes = [
  {
    path: '/users',
    component: Users,
    children: [
      {
        path: ':id',
        component: UserDetail
      }
    ]
  }
];

如果当前 URL 是 /users/123,那么 route.matched 数组将会包含两个路由记录:

索引 路由记录
0 { path: '/users', component: Users }
1 { path: ':id', component: UserDetail }

如果 RouterView 位于 Users 组件中,那么它的嵌套深度为 1,因此它应该渲染 route.matched[1] 对应的组件,也就是 UserDetail

第二幕:RouterLink 的“导航魔法”

RouterLink 的职责是生成链接,点击链接会触发路由切换。它的实现也比较简单:

// 简化版 RouterLink
const RouterLink = {
  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'
    }
  },
  computed: {
    href() {
      // 解析 to 属性,生成 href
      return this.$router.resolve(this.to).href;
    },
    isActive() {
      // 判断是否激活
      const currentRoute = this.$route;
      return this.pathToRegexp(this.href).test(currentRoute.path);
    }
  },
  methods: {
    pathToRegexp(path) {
      const escapedPath = path.replace(/[-/\^$*+?.()|[]{}]/g, '\$&');
      const regexpPattern = `^${escapedPath}$`;
      return new RegExp(regexpPattern);
    }
  },
  render(h) {
    const tag = this.tag;
    const data = {
      attrs: {
        href: this.href
      },
      class: {
        [this.activeClass]: this.isActive
      },
      on: {
        click: (e) => {
          e.preventDefault(); // 阻止默认行为
          this.$router.push(this.to); // 触发路由切换
        }
      }
    };

    return h(tag, data, this.$slots.default);
  }
};

咱们也来一行行分析:

  1. props: { to, tag, activeClass, exactActiveClass }
    • to:指定要导航到的路由。可以是字符串或对象。
    • tag:指定渲染成什么标签,默认为 a
    • activeClass:指定激活时的 CSS 类名。
    • exactActiveClass:指定精确匹配时的 CSS 类名。
  2. computed: { href, isActive }
    • href:根据 to 属性,使用 $router.resolve() 方法解析出实际的 URL。
    • isActive:判断当前路由是否激活。
  3. render(h):生成 VNode。
    • data.attrs.href:设置链接的 href 属性。
    • data.class[this.activeClass]:根据 isActive 属性,动态添加 activeClass
    • data.on.click:添加点击事件监听器,阻止默认行为,并使用 $router.push() 方法触发路由切换。

关键点:$router.push()$router.resolve()

RouterLink 的核心在于两个方法:

  • $router.push(to):触发路由切换。to 可以是字符串或对象。
  • $router.resolve(to):解析 to 属性,生成包含 hrefrouteresolved 等信息的对象。

$router.resolve() 方法非常重要,它可以将 to 属性转换为实际的 URL,并进行路由匹配。

第三幕:RouterView 和 RouterLink 的“基情互动”

现在,咱们来看看 RouterViewRouterLink 是如何协同工作的。

  1. RouterLink 生成链接RouterLink 根据 to 属性,生成链接,并设置 href 属性。
  2. 点击链接触发路由切换:当用户点击链接时,RouterLink 阻止默认行为,并调用 $router.push() 方法触发路由切换。
  3. Vue Router 更新路由对象:Vue Router 接收到路由切换的请求后,会更新当前的路由对象 ($route)。
  4. RouterView 监听路由变化RouterView 监听 $route 的变化,当路由变化时,它会重新渲染,显示匹配当前路由的组件。

这个过程可以用一个简单的流程图来表示:

sequenceDiagram
  participant RouterLink
  participant Router
  participant RouterView
  participant Component

  RouterLink->>Router: $router.push(to)
  Router->>Router: 更新 $route
  Router->>RouterView: 触发 RouterView 重新渲染
  RouterView->>Component: 渲染匹配的组件

第四幕:深入源码,揭秘细节

咱们再深入源码,看看一些细节:

  • RouterViewkeep-alive 支持RouterView 支持 keep-alive 组件,可以缓存已经渲染过的组件,提高性能。
  • RouterLinkexact 属性RouterLinkexact 属性可以控制是否进行精确匹配。如果 exact 属性为 true,那么只有当 URL 完全匹配 to 属性时,才会激活。
  • RouterLinkappend 属性RouterLinkappend 属性可以控制是否在当前 URL 的基础上追加 to 属性。

RouterView的keep-alive支持

<template>
  <router-view>
    <keep-alive>
      <component :is="$route.matched[0].components.default" />
    </keep-alive>
  </router-view>
</template>

keep-alive 是 Vue 内置的组件,用于缓存组件。当 RouterView 渲染组件时,如果组件被 keep-alive 包裹,那么组件的状态会被缓存,下次再次渲染时,会直接从缓存中读取,避免重复创建组件。

RouterLink的activeClass 和 exactActiveClass

RouterLink 根据当前路由是否激活,动态添加 activeClassexactActiveClass

  • activeClass:只要当前路由包含 to 属性,就会添加 activeClass
  • exactActiveClass:只有当当前路由完全匹配 to 属性时,才会添加 exactActiveClass

比如,有如下路由配置:

const routes = [
  {
    path: '/users',
    component: Users
  },
  {
    path: '/users/:id',
    component: UserDetail
  }
];

如果当前路由是 /users/123,那么:

  • <router-link to="/users">Users</router-link> 会添加 activeClass
  • <router-link to="/users" exact>Users</router-link> 不会添加 exactActiveClass
  • <router-link to="/users/123">UserDetail</router-link> 会添加 activeClass
  • <router-link to="/users/123" exact>UserDetail</router-link> 会添加 exactActiveClass

RouterLink的append属性

append 属性用于控制是否在当前 URL 的基础上追加 to 属性。

比如,当前 URL 是 /usersto 属性是 123,那么:

  • <router-link to="123">User</router-link> 会生成 /123
  • <router-link to="123" append>User</router-link> 会生成 /users/123

总结:RouterView 和 RouterLink 的“爱情故事”

RouterViewRouterLink 是 Vue Router 中两个至关重要的组件,它们一个负责显示内容,一个负责切换内容。它们通过 $route 对象和 $router 对象进行交互,共同完成了路由的功能。理解它们的实现原理,可以帮助我们更好地使用 Vue Router,并解决实际开发中遇到的问题。

表格总结:

组件 职责 核心属性/方法 关键点
RouterView 根据当前路由,动态渲染对应的组件 route.matched, depth 路由匹配, 嵌套深度
RouterLink 生成链接,点击链接触发路由切换 to, $router.push(), $router.resolve() 路由切换, URL 解析

希望今天的分享能帮助大家更深入地理解 Vue Router 的实现原理。如果有什么问题,欢迎随时提问。下次再见!

发表回复

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