六、router-view组件
router-view组件在执行vue.use的时候,通过Vue.component进行初始化,router-view组件的编写是通过functional式组件(使组件无状态 (没有 data) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使它们渲染的代价更小。)的方式创建组件。在RouterView的render函数中,首先会通过第二个参数中的拿到parent.$createElement,parent就是当前组件的父组件,假如我们把router-view放入app.vue中,那么他的parent就会指向app.vue。然后通过const name = props.name拿到传入的name,这是为了支持router的命名视图部分,然后执行const route = parent.$route,$route在调用install的时候通过Object.defineProperty在vue.prototype上进行了挂载,他最终的定义是this._routerRoot._route,this._routerRoot在执行beforeCreate钩子的时候会进行挂载,指向根vue实例的this,_route通过Vue.util.defineReactive挂载到了根部实例上,是一个响应式的数据,他返回的是this._router.history.current,也就是 this.$options.router.history.current,this.$options.router就是VueRouter实例,history.current就是当前的Route。之后他会首先定义一个depth,然后执行while循环,假如是app.vue中的router-view,那么当执行while中判断的时候,首先当前的parent也就是app.vue他是有的并且他的_routerRoot(vue根部实例)并不等于app.vue,所以进入循环,首先他会拿到parent.$vnode.data,判断其中有没有routerView属性,如果有的话,那么 depth++,然后让parent = parent.$parent,也就是继续向上找父组件的父组件,data.routerView定义在render函数刚开始的时候,也就是说,如果对于app.vue来说,他的首次render的父组件if (vnodeData.routerView)一定是false,也就是说,他的depth是0。那假设这样一个场景,App.vue中写着router-view组件,其中渲染/a的a组件,/a的a组件又嵌套这/a/b 的b组件,/a/b的b组件又嵌套这/a/b/c 的c组件,形成了这样一个嵌套路由的关系,那么b组件的router-view,他首先会找到b组件,b组件在执行router-view的render函数的时候会设置,data.routerView = true,这样的话,当c组件在执行render的时候parent.$vnode.routerView就可以拿到之前在执行b组件的render的data.routerView = true,也就是说会执行depth++,c组件会先找到b组件,然后parent = parent.$parent,也就是在执行while循环的时候,就成了a组件,那么a组件因为之前执行了render,也会拿到routerView是true,这样又会找到app组件,最后找到根部实例,所以c组件最终的depth为2。那么为什么要拿到depth,const matched = route.matched[depth],是为了配合之前的route.matched,matched会存放当前路由的record和他的父路由的record,也就是说,matched可以正确的取到当前的record。然后再执行const component = matched && matched.components[name],根据传入的name取到对应的component,最后执行return h(component, data, children)进行渲染。
// src/history
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
data.routerView = true
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
const h = parent.$createElement
const name = props.name
const route = parent.$route
...
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
...
const matched = route.matched[depth]
const component = matched && matched.components[name]
...
return h(component, data, children)
}
}
router-view组件中还定了data.registerRouteInstance,当组件触发beforeCreate的时候,会执行registerInstance(this, this),registerInstance函数会执行vm.$options._parentVnode.data.registerRouteInstance(vm,callVal),也就是router-view中的registerRouteInstance函数,registerRouteInstance函数会执行matched.instances[name] = val,这样就对record的instances进行了赋值,赋值为当前渲染出的组件的this。这样在执行导航守卫的时候,flatMapComponents函数就可以通过m.instances[key]拿到当前组件的this。当组件执行destroyed的时候,同样会执行 matched.instances[name] = val,把当前的instances重新赋值为空
// src/history
export default {
name: 'RouterView',
functional: true,
...
render (_, { props, children, parent, data }) {
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
}
}
// src/install.js
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
...
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
假如我当前访问的路径是/,那么router-view的render函数,在执行到const matched = route.matched[depth],此时route.matched为空,所以matched也为空,他会执行下边的return h()也就是往页面中router-view的位置,渲染了一个注释节点。那么当切换路由为/a,此时会再次触发router-view的render,render函数,只有当组件被重新渲染的时候,才会触发,那么router-view是如何做到重新渲染的。在触发vue根部实例的beforeCreate的时候,会执行 Vue.util.defineReactive(this, '_route', this._router.history.current),他会把this._route定义为一个响应式的对象,值为this._router.history.current,在router-view的render函数中,会通过const route = parent.$route,访问到this._route,触发他的getter进行依赖收集,那么当这个值发生了改变,会触发相应的渲染watcher的update,会进行页面的重新渲染。那么什么时候执行了setter,在根部vue组件执行beforeCreate的时候,会执行VueRouter的init,执行history.listen,给history的cb赋值为传入的回调函数,这个回调函数,是对app._route的重新赋值,也就是当执行完History的transitionTo中的confirmTransition的updateRoute的时候,会触发这个cb()也就是触发重新的赋值
// src/install.js
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
...
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
...
}
})
// src/history/base.js
export class History {
...
listen (cb: Function) {
this.cb = cb
}
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
// catch redirect option https://github.com/vuejs/vue-router/issues/3201
try {
route = this.router.match(location, this.current)
}
const prev = this.current
this.confirmTransition(
route,
() => {
this.updateRoute(route)
...
},
...
)
}
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
...
}
七、router-link组件
router-link组件通过render函数渲染,首先拿到router实例和当前的路径,然后调用router.resolve函数,传入this.to(props中传入),current(当前路径)和this.append(props中传入),router的resolve函数,首先通过normalizeLocation拿到location,然后通过this.match也就是this.matcher.match拿到要跳转的route,然后通过createHref把路径和history.base做一个拼接。拿到要跳转的路径之后,根据activeClass->router.options.linkActiveClass->'router-link-active'做这样的一个类名的处理,exactActiveClass也是相同的方式。exactActiveClass,通过isSameRoute函数判断跳转的路由,和当前路由他们是否完全相同,如果相同,则为true。最终把他们作为data.class,传递给createElement。接着他们声明一个handler回调函数,handler首先通过guardEvent做了一层保护,然后触发router.replace或router.push。之后给on进行赋值,根据传入的event也就是触发方式(默认click),遍历的添加到on中,把handler作为回调函数。然后会判断你传入的tag属性,默认tag是a标签。
render (h: Function) {
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
// Support global empty 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 = route.redirectedFrom
? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
}
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, 'aria-current': ariaCurrentValue }
} else {
...
if (a) {
...
} else {
...
}
}
return h(this.tag, data, this.$slots.default)
}