Appearance
RouterView详解
<router-view>
组件是一个 functional 组件,渲染路径匹配到的视图组件。<router-view>
渲染的组件还可以内嵌自己的<router-view>
,根据嵌套路径,渲染嵌套组件。
其关键点:
- 命名视图
- 组件的解耦
- 匹配深度
源码阅读
js
import { warn } from '../util/warn';
import { extend } from '../util/misc';
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
// 直接使用父上下文的createElement()函数, 这样,router-view呈现的组件就可以解析指定的插槽
// 获取父上下文的h
const h = parent.$createElement;
// 获取name属性
const name = props.name;
// 获取当前的路由对象
const route = parent.$route;
const cache = parent._routerViewCache || (parent._routerViewCache = {});
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
/*
确定当前视图的深度,也要检查树是否已切换为非活动但已激活。
view-router 最主要的是 如何在嵌套路由和命名视图的时候 知道自己应该渲染哪一个视图?
因为Vue的组件是以树的形式保存的,所以其向上进行遍历,如果遇到一个 祖先组件 其 parent.$vnode.data.routerView === true
那么说明此 祖先组件 也是一个 router-view 视图插槽,那么这时候 depth++ 就应该渲染当前route.matched 的第二个。
然后继续向上直到找到 parent._routerRoot === parent 即 路由根组件
route.matched 保存了当前路由匹配的路由对象,其顺序为 父在前子在后。 所以depth 其实就可以认为是 matched的下标
inactive 的作用?
在一个组件中可以存在多个视图插槽,如果某一个视图插槽的 组件节点 使用了 v-if 且为false 但是设置 keep-alive。 那么这时候其子孙节点应该不加载,
所以这时候就是保存了其祖先节点是否是激活的状态。如果有一个不是激活的状态那么他就不应该渲染
*/
let depth = 0;
let inactive = false;
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++;
}
// 组件节点存在不激活的状态 即 v-if='false'
// 那么这时候 这个视图插槽也不应该渲染
if (parent._inactive) {
inactive = true;
}
parent = parent.$parent;
}
// 保存 视图插槽的 深度
data.routerViewDepth = depth;
// render previous view if the tree is inactive and kept-alive
// 如果树为非活动的且kept-alive,则呈现前一个视图
if (inactive) {
return h(cache[name], data, children);
}
// 获取当前路由匹配路由 指定深度的 路由对象
const matched = route.matched[depth];
// render empty node if no matched route
if (!matched) {
cache[name] = null;
return h();
}
// 获取指定路由的 组件,并缓存到父组件的 parent._routerViewCache[name]
const component = (cache[name] = matched.components[name]);
/*
下面就是 将当前 view-router 占位符节点 渲染成指定的 component 组件
*/
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
// 附加实例注册钩子这将在实例注入的生命周期钩子中调用
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;
}
};
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
// 还可以在prepatch hook中注册实例,以防相同的组件实例在不同的路由中被重用
(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance;
};
// resolve props
/*
使用 props 将组件和路由解耦。
我们获取 动态路由的参数是 this.$router.params.xxx;
但是我们可以通过 props属性将其跟组件解耦,或变成组件的 props属性 props: ['xxx']
如果设置成 true object function 那么这时候其将会作为 data.props
*/
let propsToPass = (data.props = resolveProps(route, matched.props && matched.props[name]));
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass);
// pass non-declared props as attrs
const attrs = (data.attrs = data.attrs || {});
// 将matched.props 通过 attrs属性传递给子组件那样子组件就可以通过 props['id'...] 获取
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key];
delete propsToPass[key];
}
}
}
return h(component, data, children);
},
};
/**
* 处理 view-router中 parmas 与组件进行解耦
*
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
props : Object 那么 User组件中 { props : ['id'] }, Sidebar组件 : { this.$route.params.id}
props : function 那么
props: { default: true, sidebar: (route) => {({ orderId : route.params.id })) } 那么 Sidebar组件 : { props : ['orderId'] }
props : Boolean
props: true ; 这时候不区分命名视图,当前路由所有的视图都使用 { props : ['id'] }
* @param {*} route
* @param {*} config
* @returns
*/
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.`);
}
}
}
1. 命名视图
什么是命名视图?
命名视图: 有时候想同时 (同级) 展示多个视图,而不是嵌套展示。这时候就可以通过命名视图在同级展示多个视图组件。
栗子:
html
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar />
<router-view />
<router-view name="helper" />
</div>
js
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}
那么这时候 <router-view/>
渲染的就是 UserProfile;<router-view name="helper"/>
渲染的是 UserProfilePreview。
在阅读 VueRouter 的源码的时候知道对于视图其都存放在 route.components 属性上,虽然我们常用 component 属性去加载一个组件,但是其还是作为 route.components['default'] 上的 一个视图组件。
那么<router-view/>如何知道自己渲染哪一个视图?
其实就是借助了 name 属性,其默认属性为 default。
js
// 获取name属性
const name = props.name;
// 获取指定路由的 组件,并缓存到父组件的 parent._routerViewCache[name]
const component = (cache[name] = matched.components[name]);
然后渲染组件。
2. 组件解耦
组件解耦,这是什么意思? 其实这个意思很简单,就是让我们获取动态路由数据的时候不需要在组件中通过 this.$route.params.xxx 去获取属性的值,而是作为组件的一个 props 属性 { props: ['xxx']}。那么我们就可以直接 this.xxx 去获取这个值了。
其实是否解耦不是在组件上设置,而是在路由上设置 props
js
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>',
};
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar, menu: Menu },
props: {
default: true,
sidebar: false,
menu: route => {
route.params.id;
},
},
},
],
});
路由的 props 属性可以为 undefined, Boolean,Object,Function 中的一种。
下面看源码。
js
// resolve props
/*
使用 props 将组件和路由解耦。
我们获取 动态路由的参数是 this.$router.params.xxx;
但是我们可以通过 props属性将其跟组件解耦,或变成组件的 props属性 props: ['xxx']
如果设置成 true object function 那么这时候其将会作为 data.props
*/
let propsToPass = (data.props = resolveProps(route, matched.props && matched.props[name]));
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass);
// pass non-declared props as attrs
const attrs = (data.attrs = data.attrs || {});
// 将matched.props 通过 attrs属性传递给子组件那样子组件就可以通过 props['id'...] 获取
for (const key in propsToPass) {
// 如果组件上props没有定义此属性,那么就不需要传递
if (!component.props || !(key in component.props)) {
// 存放在 data.attrs属性上,同时也可以覆盖 router-view id='xx'时定义的属性
attrs[key] = propsToPass[key];
delete propsToPass[key];
}
}
}
其第一步 使用 resolveProps 去解析路由上的此视图的 props 值
js
/**
* 处理 view-router中 parmas 与组件进行解耦
*
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
props : Object 那么 User组件中 { props : ['id'] }, Sidebar组件 : { this.$route.params.id}
props : function 那么
props: { default: true, sidebar: (route) => {({ orderId : route.params.id })) } 那么 Sidebar组件 : { props : ['orderId'] }
props : Boolean
props: true ; 这时候不区分命名视图,当前路由所有的视图都使用 { props : ['id'] }
* @param {*} route
* @param {*} config
* @returns
*/
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.`);
}
}
}
然后生成 propsToPass 对象。
然后我们知道此时我们通过 props 传递路由动态 props 参数。父子组件也可以通过属性的方式传递。
html
<router-view id="123"></router-view>
还有就是 props 属性从占位符到组件上是通过 data.attrs 进行传递了。
所以其先获取 routerView 上原来的 attrs 属性。 然后
js
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass);
// pass non-declared props as attrs
const attrs = (data.attrs = data.attrs || {});
// 将matched.props 通过 attrs属性传递给子组件那样子组件就可以通过 props['id'...] 获取
for (const key in propsToPass) {
// 如果组件上props没有定义此属性,那么就不需要传递
if (!component.props || !(key in component.props)) {
// 存放在 data.attrs属性上,同时也可以覆盖 router-view id='xx'时定义的属性
attrs[key] = propsToPass[key];
delete propsToPass[key];
}
}
}
3. 匹配深度
什么是匹配深度?
js
render(_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
data.routerView = true
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
/*
确定当前视图的深度,也要检查树是否已切换为非活动但已激活。
view-router 最主要的是 如何在嵌套路由和命名视图的时候 知道自己应该渲染哪一个视图?
因为Vue的组件是以树的形式保存的,所以其向上进行遍历,如果遇到一个 祖先组件 其 parent.$vnode.data.routerView === true
那么说明此 祖先组件 也是一个 router-view 视图插槽,那么这时候 depth++ 就应该渲染当前route.matched 的第二个。
然后继续向上直到找到 parent._routerRoot === parent 即 路由根组件
route.matched 保存了当前路由匹配的路由对象,其顺序为 父在前子在后。 所以depth 其实就可以认为是 matched的下标
inactive 的作用?
在一个组件中可以存在多个视图插槽,如果某一个视图插槽的 组件节点 使用了 v-if 且为false 但是设置 keep-alive。 那么这时候其子孙节点应该不加载,
所以这时候就是保存了其祖先节点是否是激活的状态。如果有一个不是激活的状态那么他就不应该渲染
*/
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
// 组件节点存在不激活的状态 即 v-if='false'
// 那么这时候 这个视图插槽也不应该渲染
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
// 保存 视图插槽的 深度
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
// 如果树为非活动的且kept-alive,则呈现前一个视图
if (inactive) {
return h(cache[name], data, children)
}
// 获取当前路由匹配路由 指定深度的 路由对象
const matched = route.matched[depth];
// render empty node if no matched route
if (!matched) {
cache[name] = null
return h()
}
return h(component, data, children)
}
其通过 data.routerView = true 标记当前组件为 routerView 组件,然后通过向上遍历查找,如果遇到 parent.$vnode.data.routerView 那么说明其存在 父 RouterView ,继续向上直到找到 parent._routerRoot === parent 即 路由根组件。
这样 depth 就标记出当前 RouterView 的深度。
然后 route.matched 保存了当前路由匹配的路由对象,其顺序为 父在前子在后。 所以 depth 其实就可以认为是 matched 的下标。
我们我们就知道 depth 就可以作为 matched 的下标,从而获取到当前深度的组件对象