各位靓仔靓女,今天咱们来聊聊Vue Router里两个相当重要的家伙:RouterView
和 RouterLink
。这两个组件,一个负责展示页面,一个负责导航跳转,简直就是Vue路由的左膀右臂。咱们从源码的角度,好好扒一扒它们是如何工作的,以及它们和路由实例之间那些不得不说的故事。
一、RouterView:页面展示的“容器”
首先,我们来看看RouterView
。你可以把它想象成一个舞台,路由切换的时候,不同的组件就在这个舞台上轮番登场。
- 基础结构:一个 Functional Component
在Vue Router的源码里,RouterView
通常被实现为一个函数式组件。为什么要用函数式组件呢?主要是因为RouterView
本身不需要管理任何状态,它仅仅是根据当前的路由信息,动态渲染对应的组件。这样可以减少一些不必要的开销,提高性能。
// 源码简化版,仅供参考
export const RouterView = {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (h, { parent, data, props }) {
// ... 渲染逻辑
}
}
这里,functional: true
告诉Vue这是一个函数式组件。props
定义了组件接收的属性,name
属性允许我们使用具名视图,也就是在同一个页面渲染多个路由组件。render
函数是组件的核心,它负责生成虚拟DOM。
- 与路由实例的“亲密接触”
RouterView
组件要展示哪个组件,得知道当前的路由信息才行。它通过inject
选项,从父组件(通常是根组件)注入$router
和$route
。
$router
:路由实例,负责路由的管理和跳转。$route
:当前激活的路由对象,包含路由的路径、参数等信息。
- 渲染的核心逻辑
RouterView
的render
函数会根据$route.matched
数组,递归地渲染匹配到的组件。$route.matched
是一个数组,包含了从根路由到当前路由的所有匹配到的路由记录。
// render函数简化版
render (h, { parent, data, props }) {
const route = parent.$route
const matched = route.matched
const name = props.name
const depth = 0 // 可以根据实际情况计算深度,这里简化为0
// 找到要渲染的组件
let component = null
if (matched && matched.length) {
const record = matched[depth]
component = record && record.components[name]
}
// 如果没有匹配到组件,渲染一个空的VNode
if (!component) {
return h()
}
// 渲染组件
return h(component, data)
}
这段代码做了以下几件事:
- 获取当前路由对象
route
和匹配到的路由记录matched
。 - 根据
name
属性,找到要渲染的组件。 - 如果找到了组件,就用
h
函数创建VNode,并渲染它。 - 如果没有找到组件,就渲染一个空的VNode,避免出错。
- 处理嵌套路由
RouterView
一个很重要的功能是处理嵌套路由。当路由嵌套时,$route.matched
数组会包含多个路由记录,RouterView
需要递归地渲染这些路由记录对应的组件。
为了实现递归渲染,RouterView
组件会把自己也渲染成一个组件,作为当前组件的子组件。这样,就可以在父组件中嵌套RouterView
,从而实现嵌套路由的效果。
// render函数简化版,包含递归渲染逻辑
render (h, { parent, data, props }) {
const route = parent.$route
const matched = route.matched
const name = props.name
let depth = 0 // 可以根据实际情况计算深度,这里简化为0
// 找到要渲染的组件
let component = null
if (matched && matched.length) {
const record = matched[depth]
component = record && record.components[name]
}
// 如果没有匹配到组件,渲染一个空的VNode
if (!component) {
return h()
}
// 创建RouterView的data对象,用于传递给子RouterView
const routerViewData = {
props: {
name: name
},
// 传递当前路由的matched和深度给子RouterView
routerViewRoute: route,
routerViewDepth: depth + 1
}
// 渲染组件,并将RouterView作为子组件
return h(component, data, [
h(RouterView, routerViewData) // 递归渲染
])
}
注意这里递归渲染的h(RouterView, routerViewData)
。每个RouterView
都会根据自己的depth
,找到对应的路由记录,并渲染相应的组件。
- 缓存组件(keep-alive)
RouterView
还支持使用keep-alive
组件来缓存组件。这样,当路由切换时,被缓存的组件不会被销毁,而是会被保存在内存中,下次再访问时可以直接从缓存中取出,提高性能。
<keep-alive>
<router-view></router-view>
</keep-alive>
RouterView
内部会判断父组件是否使用了keep-alive
,如果使用了,就会将当前组件缓存起来。
二、RouterLink:导航跳转的“指挥棒”
接下来,我们来看看RouterLink
。它负责生成带有正确链接的<a>
标签,点击这些标签可以触发路由跳转。
- 基础结构:一个普通组件
与RouterView
不同,RouterLink
是一个普通的Vue组件,它需要管理一些状态,比如是否是激活状态。
// 源码简化版,仅供参考
export default {
name: 'RouterLink',
props: {
to: {
type: [String, Object],
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
event: {
type: [String, Array],
default: 'click'
}
},
computed: {
// ... 计算属性
},
methods: {
// ... 方法
},
render (h) {
// ... 渲染逻辑
}
}
props
定义了组件接收的属性:
to
:要跳转的路由地址,可以是字符串或对象。tag
:渲染的标签类型,默认为<a>
。exact
:是否精确匹配路由。append
:是否在当前路径后追加路径。replace
:是否使用replaceState
替换历史记录。activeClass
:激活时的CSS类名。event
:触发路由跳转的事件,默认为click
。
- 与路由实例的“甜蜜互动”
RouterLink
组件同样通过inject
选项,从父组件注入$router
和$route
。
- 计算属性:生成链接和激活状态
RouterLink
组件定义了一些计算属性,用于生成链接和判断是否是激活状态。
href
:根据to
属性,生成最终的链接地址。classes
:根据当前路由和activeClass
属性,生成CSS类名。
- 渲染的核心逻辑
RouterLink
的render
函数会根据tag
属性,生成对应的HTML标签,并绑定事件处理函数。
// render函数简化版
render (h) {
const tag = this.tag
const data = {}
const route = this.$router.resolve(this.to)
const href = route.href
const classes = {}
// 设置CSS类名
if (this.activeClass && this.isActive) {
classes[this.activeClass] = true
}
data.class = classes
// 设置链接属性
data.attrs = { href }
// 绑定事件处理函数
const on = {
click: (e) => {
e.preventDefault()
if (this.replace) {
this.$router.replace(this.to)
} else {
this.$router.push(this.to)
}
}
}
data.on = on
return h(tag, data, this.$slots.default)
}
这段代码做了以下几件事:
- 根据
to
属性,使用$router.resolve
方法生成路由信息。 - 获取链接地址
href
。 - 根据
activeClass
属性和isActive
计算属性,设置CSS类名。 - 绑定
click
事件处理函数,点击链接时触发路由跳转。 - 使用
h
函数创建VNode,并渲染它。
- 点击事件处理
RouterLink
最核心的功能就是处理点击事件,触发路由跳转。
当点击RouterLink
生成的链接时,会触发click
事件处理函数。这个函数会阻止默认的链接跳转行为,然后调用$router.push
或$router.replace
方法,触发路由跳转。
$router.push
:向历史记录中添加一条新的记录,可以使用浏览器的前进/后退按钮。$router.replace
:替换当前的记录,不能使用浏览器的前进/后退按钮。
$router.resolve
的作用
$router.resolve
方法非常重要,它可以将to
属性(可以是字符串或对象)转换为一个包含href
、route
等信息的对象。这样,RouterLink
就可以生成正确的链接地址,并判断是否是激活状态。
三、RouterView 和 RouterLink 如何协同工作
RouterView
负责展示页面,RouterLink
负责导航跳转,它们是如何协同工作的呢?
- 用户点击RouterLink
当用户点击RouterLink
生成的链接时,会触发click
事件处理函数。
- RouterLink触发路由跳转
RouterLink
的click
事件处理函数会调用$router.push
或$router.replace
方法,触发路由跳转。
- 路由实例更新当前路由
$router.push
或$router.replace
方法会更新路由实例的currentRoute
属性,也就是$route
对象。
- RouterView响应路由变化
RouterView
组件会监听$route
对象的变化。当$route
对象发生变化时,RouterView
会重新渲染,展示新的组件。
- 组件渲染完成,页面更新
新的组件被渲染到页面上,用户看到新的页面内容。
可以用表格来总结一下:
步骤 | 组件 | 动作 | 影响 |
---|---|---|---|
1 | RouterLink | 用户点击链接 | 触发click事件 |
2 | RouterLink | 调用$router.push 或$router.replace |
触发路由跳转 |
3 | Router | 更新currentRoute ($route ) |
$route 对象发生变化 |
4 | RouterView | 监听$route 的变化 |
重新渲染,展示新的组件 |
5 | 组件 | 渲染到页面上 | 用户看到新的页面内容 |
四、源码分析总结
总的来说,RouterView
和RouterLink
这两个组件,分别负责页面展示和导航跳转,它们通过与路由实例的交互,实现了Vue Router的核心功能。
RouterView
是一个函数式组件,负责根据当前的路由信息,动态渲染对应的组件。它通过inject
选项,从父组件注入$router
和$route
,并递归地渲染匹配到的组件。RouterLink
是一个普通组件,负责生成带有正确链接的<a>
标签,点击这些标签可以触发路由跳转。它同样通过inject
选项,从父组件注入$router
和$route
,并绑定click
事件处理函数,触发路由跳转。
这两个组件的实现,充分体现了Vue的组件化思想和响应式原理。通过组件的组合和数据的驱动,我们可以轻松地构建复杂的单页应用。
五、一些小提示和注意事项
- 理解
$route.matched
数组的结构,对于理解嵌套路由的实现至关重要。 $router.resolve
方法是生成链接地址的关键,要熟悉它的用法。keep-alive
组件可以缓存组件,提高性能,但也要注意缓存带来的问题,比如组件的状态可能会过期。- 可以自定义
RouterLink
组件,实现更高级的导航功能。
好了,今天的分享就到这里。希望通过今天的讲解,大家对Vue Router的RouterView
和RouterLink
组件有了更深入的了解。下次再见!