vue-router 源码分析-组件
dwqs opened this issue · comments
我是星礼 commented
在上篇中, 大致分析了 vue-router
的整体流程. VueRouter 除了提供 router
功能, 还提供了两个组件: router-link
和 router-view
, 源码均在 src/components
目录下.
本文分析的 vue-router 的版本为 2.6.0.
Link
<router-link>
组件通过 to
属性指定目标地址, 为用户提供路由导航. 默认渲染成带有正确链接的 <a>
标签. 其源码在 src/components/link.js
:
/* @flow */
import {createRoute, isSameRoute, isIncludedRoute} from '../util/route'
import {_Vue} from '../install'
// 类型定义
const toTypes: Array<Function> = [String, Object]
const eventTypes: Array<Function> = [String, Array]
export default {
// 组件名
name: 'router-link',
props: {
// 目标路由
to: {
type: toTypes,
required: true
},
// 目标标签
tag: {
type: String,
default: 'a'
},
// 完整模式, 如果为 true 那么也就意味着
// 绝对相等的路由才会增加 activeClass
// 否则是包含关系, 默认是 false
exact: Boolean,
// 是否在当前路径路径添加基路径 默认 false
append: Boolean,
// 是否使用 router.replace() 来替换 router.push() 默认 false
replace: Boolean,
// 链接激活时使用的 CSS 类名
activeClass: String,
// 完整模式下链接激活时使用的 CSS 类名
exactActiveClass: String,
// 触发导航的事件
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
// 得到 router 实例以及当前激活的 route 对象
const router = this.$router
const current = this.$route
// 获取当前匹配的 route信息
const {location, route, href} = router.resolve(this.to, current, this.append)
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
// 获取 active class
const activeClassFallback = globalActiveClass == null
? 'router-link-active'
: globalActiveClass
const exactActiveClassFallback = globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass
const exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget = location.path
? createRoute(null, location, null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
// 完成模式还是包含模式
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
// 事件处理
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
// 事件监听
const on = {click: guardEvent}
if (Array.isArray(this.event)) {
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
// 创建元素需要附加的数据
const data: any = {
class: classes
}
if (this.tag === 'a') {
data.on = on
data.attrs = {href}
} else {
// 找到第一个 <a> 并绑定事件和 href 属性
const a = findAnchor(this.$slots.default)
if (a) {
// in case the <a> is a static node
a.isStatic = false
// 用于属性扩展
const extend = _Vue.util.extend
const aData = a.data = extend({}, a.data)
aData.on = on
const aAttrs = a.data.attrs = extend({}, a.data.attrs)
aAttrs.href = href
} else {
// 没找到就给当前元素自身绑定事件
data.on = on
}
}
// 创建元素
return h(this.tag, data, this.$slots.default)
}
}
// router-link 的 event 绑定
function guardEvent(e) {
// 忽略功能键的点击跳转
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
// 已经阻止
if (e.defaultPrevented) return
// 右击不跳转
if (e.button !== undefined && e.button !== 0) return
// 忽略 `target="_blank"
if (e.currentTarget && e.currentTarget.getAttribute) {
const target = e.currentTarget.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
}
// 阻止默认行为
if (e.preventDefault) {
e.preventDefault()
}
return true
}
function findAnchor(children) {
if (children) {
let child
for (let i = 0; i < children.length; i++) {
child = children[i]
if (child.tag === 'a') {
return child
}
if (child.children && (child = findAnchor(child.children))) {
return child
}
}
}
}
从上述代码可以看出, router-link
组件会根据绑定的事件类型和 to
属性, 去调用 push
或者 replace
更新路由, 同时根据 exact
属性来添加 active class
.
View 组件
router-view
组件用于渲染与路由匹配的 components
, 其源码在 src/components/view.js
中定义的:
import {warn} from '../util/warn'
export default {
// 组件名
name: 'router-view',
// 显示指定为该组件是函数式组件
// 函数式组件: https://cn.vuejs.org/v2/guide/render-function.html#函数化组件
functional: true,
props: {
// 视图名称, 默认是 default
name: {
type: String,
default: 'default'
}
},
render (_, {props, children, parent, data}) {
data.routerView = true
// 渲染函数
const h = parent.$createElement
const name = props.name
// route 对象
const route = parent.$route
// 缓存
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// 组件所在深度
let depth = 0
let inactive = false
// 当 _routerRoot 指向 Vue 实例时就终止循环
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
// 处理 keep-alive 组件
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// 渲染缓存的 keep-alive 组件
if (inactive) {
return h(cache[name], data, children)
}
// 根据组件深度获取对应的 route
const matched = route.matched[depth]
if (!matched) {
// 没有对应的 route 就渲染一个空节点
cache[name] = null
return h()
}
// 得到要渲染组件
const component = cache[name] = matched.components[name]
// 添加注册钩子, 钩子会被注入到组件的生命周期钩子中
// 在 src/install.js, 会在 beforeCreate 钩子中调用
data.registerRouteInstance = (vm, val) => {
// val 为空就注销注册
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
// 在 prepatch 钩子中注册组件实例,
// 因为不同路由可能使用同一个组件, 便于组件复用
;
(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// resolve props
data.props = resolveProps(route, matched.props && matched.props[name])
// 调用 createElement 函数 渲染匹配的组件
return h(component, data, children)
}
}
function resolveProps(route, config) {
switch (typeof config) {
case 'undefined':
return
case 'object':
return config
case 'function':
return config(route)
case 'boolean':
return config ? route.params : undefined
default:
if (process.env.NODE_ENV !== 'production') {
warn(
false,
`props in "${route.path}" is a ${typeof config}, ` +
`expecting an object, function or boolean.`
)
}
}
}
router-view
被定义为一个无状态组件, 因为 router-view
只是一个函数, 用于渲染与路由对应的组件, 不需要管理或者监听任何传递给它的状态, 也没有生命周期方法, 所以渲染开销会低很多.
相关文章
cobish commented
有两个疑惑,像以下这些赋值给 data 的属性是给谁用的?
data.routerViewDepth = depth
data.hook = {}
还有 parent 自带的一些属性是 vue 里自带的吗?
parent._routerViewCache
parent._inactive