vue-router源码解析系列。这是第一篇。本篇介绍源码中的install.js
和components/view.js
(也就是router-view
)。即使对vue-router全部源码不敢兴趣,本篇内容也能帮助你更深入理解router-view
。跟其它对vue-router源码的解读不一样,我是先从router-view
着手的。本系列解析的是官方git库中3.1.6的版本源码。要学好源码,必须先掌握vue-router
的运用。
install.js
vue-router
是需要以插件的形式,安装到基于vue
的app中的。这里的app以及以后的app,指的不是安卓和ios的app,而是指vue
开发的网页应用。install.js
就是vue-router
源码中按照vue
插件开发要求写的插件实现。源码链接:install.js。
代码不多,我拆分要点总结。在看他人源码分析前,其实自己应该要先去做学习,可能你自己本身就能学好,看别人的东西,更多的作用是查漏补缺。
1 | export let _Vue |
这段是避免重复安装。 说实话没有它也没关系,只是框架开发者从自身角度,在帮助开发者减少错误。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const 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 () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
这一段是有核心作用的。首先this.$options.router
这个数据,是指在构建vue
实例的时候传入的options.router
,一般app都是下面的方式实例化并且传入router
实例:1
2
3
4
5
6new Vue({
router, // vue-router实例,按照vue-router的使用方式构建,通常在router/index.js文件
render(h) {
return h(App)
}
}).$mount('#app')
从beforeCreate
的代码可以看到,这段代码:1
2
3
4this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
只在实例化时传入了router
这个option的vue
实例内才会执行,而这种实例通常都是app实例;普通的实例则通过下面这样代码:1
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
依赖vue实例的parent
引用,一级一级地引用至根级别的vue实例(也就是app实例上)的_routerRoot
属性,从而保证了app实例范围内,所有的vue实例都能通过_routerRoot
访问到同一个对象。1
2
3
4this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
这四行代码有四个作用:
- 在app实例上定义了一个
_routerRoot
属性,指向自己 - 在app实例上定义了一个
_router
属性,指向vue-router
实例 - 执行了
router
的初始化,也就是路由的初始化,这样app的路由能力就起来了 - 在app实例上定义了一个响应式属性
_route
,初值为this._router.history.current
,也就是当前的route
对象;route对象是什么,route对象是vue-router
在内部做了路径匹配之后创建的一个对象,包含了当前路由相关的所有信息;这个_route
是响应式的,意味着对这个属性的修改,将引发app
实例的render
。 这点非常重要!
在前面代码中,可以看到在beforeCreate
和destroyed
两个钩子函数中,都最后调用了这个函数:1
2
3
4
5
6const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
这个函数有它关键性的作用,它表面上其实就是为了调用最终得到一个registerRouteInstance
方法,而这个方法是在router-view
的源码中定义的,所以在学route-view
的时候,反过来解释比较好。
install.js
还有以下一段代码,值得解析:1
2
3
4
5
6
7Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
它的作用是在vue
的原型对象上,分别定义了两个属性$router
和$route
,这就是我们在使用vue-router
的时候,为什么在所有.vue
文件里,都能通过this.$route
和this.$router
拿到当前的route
对象和vue-router
实例的根本原因。而$route
的本质,其实就是前面代码中定义的那个响应式属性_route
,$router
本质上就是在beforeCreate
里面通过options.router
传到app实例上的vue-router
实例。
router-view
router-view
是vue-router
提供的一个内置组件,用来渲染路由所匹配的vue
组件,它所在的源码文件为:components/views.js。理解它的源码,对于深入掌握vue-router
的用法有很多帮助。它的代码量也不多。
整体上的,它的结构为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import { 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 }) {
data.routerView = true
const h = parent.$createElement
// ...
return h(component, data, children)
}
}
warn
是一个在别的源码中的定义的工具函数,用来打印日志;extend
也是别的地方定义的一个工具函数,用来进行属性的拷贝,类似Object.extend
。通过上面的代码结构可以看到,router-view
其实是一个函数式组件,functional
这个option
设置为true
,所以它本身不会进行实际渲染,而是通过render
函数内部逻辑,渲染其它的component
。 router-view
的render函数写法,也是vue
官方文档中对于函数式组件
说明的标准的render
函数写法,第二个参数是一个context
对象,描述了函数式组件在被render
时的上下文,只不过源码中采用了函数参数解构的方式来定义了props children parent data
这几个参数。 这个几个参数的具体含义从名字也能知其一二,详细地说明,可前往vue
官方文档进行了解。
render
函数的最后调用了h(component, data, children)
,说明render
函数最终是通过创建其它component
的vnode
来完成渲染的,函数式组件就是这样,它一定是用来创建其它组件的vnode,而不是自己的,自己的意义不大。
另外从这段代码也能看到,router-view
仅定义了一个属性name
,这也是在使用vue-router
中经常用的:1
<router-view name="main"></router-view>
这个name
会传入到props
中。在后续的render
逻辑中有重要作用。
接下来分段解析render
函数中的代码。1
data.routerView = true
从这里开始注意,data
这个变量,在render
函数中,代表的是vnode
的数据对象,它不是vue
实例的那个数据对象,在vue
的render函数中,更多的是跟vnode
的逻辑,而不是vue
实例的逻辑。而且vue
的核心数据结构,是vnode
的树结构,而不是我们熟知的dom结构。app中的页面,是根据vnode tree
渲染出来的。而vnode tree
也不仅仅用于渲染dom,在官方的devtools
中,有一个类似elements
的调试面板,开发者工具中可见,它是以vnode tree
结构来描述页面的,data.routerView
的作用,可以在devtools
中一目了然的看到哪个vnode
是router-view
所渲染出来的。
接下来是这段代码:1
2
3
4
5
6// 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
const cache = parent._routerViewCache || (parent._routerViewCache = {})
首先明确parent
是一个vue
实例,而不是vnode
对象。这段代码有很多的要点。 先来看第一行:1
const h = parent.$createElement
这行代码定义了一个h
变量来作为render
中函数中最终进行vnode
创建的函数,而不是render
函数的第一个参数。 根据注释我们能知道原因,说明如果不用parent.$createElement
,router-view
所渲染的组件,是不具备解析具名插槽的能力的。 这是一种什么样的使用场景呢?
原来在大多数场景中,我们使用router-view
,都是把它当成一个单一标签来用的:1
<router-view></router-view>
殊不知,router-view
是可以像下面一样的使用的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- 定义router-view -->
<router-view title="test">
<template #test="{avatar}">
<img :src="avatar">
</template>
<p>流云诸葛</p>
</router-view>
<!-- 被上面的router-view渲染的某个component -->
<template>
<div>
<slot></slot>
<slot name="test" :avatar="'http://...'"></slot>
</div>
</template>
在这种使用过程中:const h = parent.$createElement
是会起到作用的,因为component
的vnode,是通过router-view
父组件的createElement
创建的。
另外还能看到除了name
会作为props
外,router-view
组件还能定义其它attributes
,比如上面的title
。最终这些attributes
和具名插槽,都会进入router-view
函数中的data
变量,而默认插槽,则会进入children
变量,最后调用h(component, data, children)
时,它们也就在实际渲染的component
里面生效了。
再看第二行:1
const name = props.name
这个的话,拿到了当前router-view
的名字,在后续逻辑中有重要作用,因为vue-router
中,定义路由时,一个路由可以匹配到多个router-view
组件,然后每个router-view
组件,渲染不同的component
:1
2
3
4
5
6
7
8
9
10
11
12const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
那显然,一个router-view
组件是只能渲染一个component
的,那它该渲染谁呢?这个对应关系就是通过name
属性建立的。
第三行代码:1
const route = parent.$route
这行代码拿到了parent
节点实例的$route
属性,这个属性在上面的install.js
中已经知道它是干什么的,反正拿到它,就能拿到当前vue-router
框架下,根据路径所匹配到的路由数据,也就是一个route
对象。这行代码看起来很简单,实际上作用也非常大,暂时不介绍,到后面其它代码会与其有关。
第四行代码:1
const cache = parent._routerViewCache || (parent._routerViewCache = {})
这行代码在父组件实例上定义了一个_routerViewCache
的对象,从名称就能看到,它的作用是用来做缓存管理的。那么什么场景需要这个对象呢?这个跟后面的代码有关,后面着重拿出来分析,并且会告诉你有哪个场景与它对应。 首先可以明确的是,vue
是一个不断进行render
的模式,每个vue
实例都有自己的生命周期,这里面的parent
也是,所以如果parent._routerViewCache
要起到实际价值的话,parent
实例本身就必须一直存活着才行,所以这个_routerViewCache
是与keep-alive
有关的。
接下来分析这段代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
let depth = 0
let inactive = false
// parent._routerRoot === parent 说明parent是app实例
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
}
// 估计也跟devtools有关,暂时没看到它的用途
data.routerViewDepth = depth
这段代码的作用就是为了得到两个状态:depth
和inactive
。
depth
是个什么概念?当router-view
位于app顶级组件中,这个router-view
的depth
就是0,但是如果router-view
存在嵌套,那么嵌套的子router-view
在执行render
函数时,它的depth
就不再是0了。如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<!-- App.vue -->
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<!-- Foo.vue -->
<template>
<div>
<router-view></router-view>
</div>
</template>
<!-- Bar.vue -->
<template>
<div>
<router-view></router-view>
</div>
</template>
假如以上三个组件中的router-view
存在嵌套关系,App.vue -> Foo.vue -> Bar.vue
,我可以这么定义路由:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
path: '/',
name: 'index',
component: import(/* webpackChunkName: "detail" */ '../page/Foo.vue'),
children: [
{
path: 'foo',
name: 'foo',
component: import(/* webpackChunkName: "detail" */ '../pages/Bar.vue'),
children: [
{
path: 'bar',
name: 'bar',
component: import(/* webpackChunkName: "detail" */ '../pages/Conent.vue'),
}
]
}
]
}
当你访问/foo/bar
时,最终会按照App.vue -> Foo.vue -> Bar.vue
的先后关系完成渲染,这样的话,一共会有三个router-view
组件被渲染,第一个实际渲染的是Foo.vue
,第二个实际渲染的是Bar.vue
,第三个实际渲染的是Content.vue
。 而这三个router-view
在执行render
函数时,运行到depth
相关的那个while
函数,最终depth
的值分别就是0 1 2
。1
2
3
4const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
vnodeData
是拿到了父组件实例的vnode
的数据对象,如果父组件实例是通过router-view
渲染的,那么vnodeData.routerView
一定为true,看render
函数的第一行就知道了。
depth
有什么作用呢?它跟const route = parent.$route
得到的route
变量使用有关,后面有一行代码:1
const matched = route.matched[depth]
这里就是depth
起作用的地方,route
变量是vue-router
内部核心代码所创建的一个描述路由信息的数据对象,它有一个matched
属性,这是一个数组,存放vue-router
经过路由匹配解析之后的route信息。 像上面举例的那种路由配置结构,当访问/foo/bar
时,所对应的route.matched
会包含三个元素:
由此可见depth
与route.matched
中的元素顺序正好是对应的,所以通过route.matched[depth]
就能找到与当前router-view
组件对应的route
配置信息。 至于为什么它们有这种关联,显然跟route
对象的创建过程有关,这就是vue-route
其它代码的功劳了。
inactive
这个变量也是在前面这个while
循环中赋值的,因为它跟depth
状态一样,都是网上遍历tree
得出的。inactive
为true
的含义是,当前的router-view
组件,正处于一个keep-alive
模式下且当前是非激活状态的tree
当中。 后面单独来介绍它的应用场景。
接下来看这段代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const matched = route.matched[depth]
const component = matched && matched.components[name]
// render empty node if no matched route or no config component
if (!matched || !component) {
cache[name] = null
return h()
}
// cache component
cache[name] = { component }
const configProps = matched.props && matched.props[name]
// save route and configProps in cachce
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
return h(component, data, children)
以上是render
函数的创建component
的vnode
的基础代码,为了不受其它代码的干扰,我暂时把其它不相干的代码移除掉了,它们会在后面的部分来单独说明。
首先:1
2const matched = route.matched[depth]
const component = matched && matched.components[name]
cache
是parent._routerViewCache
,前面代码已经定义了的。通过route
和depth
能拿到当前route-view
所匹配的相关route
配置,也就是matched
变量所指向的,这个matched
包含的就是类似下面的数据:matched
上面的components
和instances
都是在router-view
源码中要用到的。而matched.components
实际上就是路由配置时定义的组件配置,如:1
2
3
4
5
6
7
8{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
上面这个配置距离是单路由多视图的配置,如果是单视图配置,component名称默认就是default
。 所以最后通过这行代码:const component = matched && matched.components[name]
就拿到了当前router-view
实际要渲染的组件了。
剩下这段就好理解:1
2
3
4
5
6
7
8// 没有匹配到路由,或者匹配到路由,没有当前的router-view定义相应的component
if (!matched || !component) {
cache[name] = null
return h()
}
// 将当前要渲染的component以name为键名,缓存在父节点内
cache[name] = { component }
接下来这段代码:1
2
3
4
5
6
7
8
9
10
11const configProps = matched.props && matched.props[name]
// save route and configProps in cachce
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
return h(component, data, children)
configProps
是解析出路由配置中的props
数据,vue-router
中有这块的文档说明,通过这处代码,我们能更加清晰地理解官方的文档说明。
只有configProps
为trusy,下面的代码才会继续:1
2
3
4
5extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
extend
的作用,就是把当前的route
和configProps
数据,也添加到缓存中去,这样cache[name]
里缓存的就有:component route configProps
。
fillPropsinData
是解析props
数据,并把这些数据,放入到data.props
,这样就完成路由参数到组件props
的解耦。 它依赖的是fillPropsinData
、resolveProps
这2个函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37function fillPropsinData (component, data, route, configProps) {
// resolve props
let propsToPass = data.props = resolveProps(route, configProps)
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass)
// pass non-declared props as attrs
const attrs = data.attrs = data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
}
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.`
)
}
}
}
这两个函数不难理解,所以不过多说明,补充2个要点:
- 从resolveProps就能理解官方文档里面提到了那些布尔模式、函数模式和对象模式了
- fillPropsinData内部,会根据
component.props
的定义,来决定把哪些数据放到data.props
,未在component.props
里定义的configProps
解析出的数据,将会放入data.attrs
最后一行代码:1
return h(component, data, children)
就是创建好了实际component的vnode
节点,下一步就是实例化相应的component
,并且执行它的render
方法完成最终渲染了。
接下来看这段代码,为了引用方便,给它起个代号A:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if (inactive) {
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
} else {
// render previous empty view
return h()
}
}
这段代码把前面暂时性挂起的一些内容,全部关联起来了:inactive
和cache
的作用。从表面上看,它不难理解,就是在inactive
为true
时,从父节点读缓存,如果命中缓存,则以缓存的数据调用h(cachedComponent, data, children)
并返回,否则就创建空白节点。难点在于:
- 什么场景下会导致以上代码执行
- 什么时候会进入h(cachedComponent, data, children)
#2301
指的是什么
实际问题比上面还多些,我一一来解析。先来看什么场景下会导致以上代码执行
,我给你准备下面这个配置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const Home = {
render(h) {
return h('div', [
h('h1', ['Home']),
h('p', [h('router-link', { props: { to: '/home/info' } }, ['Info'])]),
h('router-view', {
props:{
name: 'sub'
}
})
])
},
created() {
console.log(this.test)
},
props: {
test: {
required: true,
type: String
}
}
}
const Foo = {
render(h) {
return h('div', ['Foo'])
}
}
const Info = {
render(h) {
return h('div', ['Info'])
},
props: {
test: {
required: true,
type: String
}
},
created() {
console.log(this.test)
}
}
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/home',
component: Home,
props: {
test: 'Test'
},
children: [
{
path: 'info',
components: {
sub: Info
},
props: {
sub: {
test: 'Test'
}
}
}
]
},
{ path: '/foo', component: Foo }
]
})
export default router
App.vue
:1
2
3
4
5
6
7
8
9<template>
<div id="app">
<router-link to="/home">/home</router-link>
<router-link to="/foo">/foo</router-link>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>
你可以拿去做demo测试。这里面有三个路由:/home /home/info /foo
,且app内使用了keep-alive
,当你运行起来后,先访问/home/info
,然后再访问/foo
,就会触发我们要分析那段源码A的执行。因为/home
跟/home/info
是一个嵌套关系,涉及到两个route-view
组件,当你访问/home/info
时,两个route-view
组件的render
都会执行,并且都会把当前的component
缓存到父级节点实例内,这是我们前面所有的代码学习已经明确了的。当你通过链接访问到/foo
时,这两个router-view
会发生什么:
- 第一个
router-view
会执行render
,但是它会创建一个新的vnode
,用来渲染Foo
组件; - 第二个
router-view
会执行render
,但是它会执行源码A,而不是渲染什么新的component。
第二个router-view
执行过程的逻辑是:1
2
3
4
5
6
7
8
9
10const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
}
这个过程里,cache
发挥了作用,如果从缓存中读取了component route configProps
,就可以按照渲染新component的逻辑,来调用h(cachedComponent, data, children)
创建节点。cachedComponent
有值很重要,说明在这种嵌套的router-view
加keep-alive
场景中,只有被嵌套的router-view
发生过渲染才会执行,假如前面的测试顺序是先访问/foo
,再访问/home/info
,就不会触发源码A的执行。
接下来有个非常关键的问题,为什么在keep-alive
变为inactive
时,第二个router-view
的render
函数会执行呢?一个被inactive的组件实例的render
方法被执行,这不符合常理阿!经过一段时间的代码分析和调试,最终发现原因是这行代码:1
const route = parent.$route
为什么呢?因为parent.$route
并不是一个简单的属性,而是在访问前面install.js
中定义的那个响应式属性_route
:1
Vue.util.defineReactive(this, '_route', this._router.history.current)
在vue
的内部代码中,定义响应式属性的过程,实际上是重新定义setter
和getter
的过程。parent.$route
最终访问的是_route
这个响应式数据的getter
,而vue
在定义响应式数据时的getter
源码是这样的:
我框出了关键性的代码,此处不会深入去分析它,正是因为框出的这段代码的作用,导致parent.$route
的访问,会建立parent
与_route
之间的依赖关系,当_route
变化的时候,parent
就会render
!这才是router-view
组件只要渲染过一次,在route变化时,router-view
组件无论是什么情况,都会重新render
的根本原因。
下面一个问题:为什么不直接返回h()
,而是要利用缓存呢,反正在源码A执行后所返回的这个vnode也是inactive
的,就是不可见的。因为如果直接返回h(),则会导致cachedComponent本应该保持的状态丢失,也就是会把inactive
的节点实例给销毁了。当cachedComponent从inactive恢复到active时,之前的状态就都丢了。从devtools调试发现,当直接return h(),router-view对应的节点已经不再是之前的component了。这会导致keep-alive的不一致性。
最后来看看#2301
是什么回事,这个其实是一个issue
的id,你访问: https://github.com/vuejs/vue-router/issues/2301就能查看详情,下面这段代码:1
2
3if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
是为了解决#2301
提到的bug的。我翻了以前的一些版本源码,上面这段是没有的,这是后期做bug修复添加的。
来看最后一部分源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// 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
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// register instance in init hook
// in case kept-alive component be actived when routes changed
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
}
这个部分的代码作用是一样的,只是场景不同的。 首先是data.registerRouteInstance
,这个钩子函数,要跟install.js
中的这段结合起来看:1
2
3
4
5
6const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
registerInstance
在vue实例的beforeCreate
和destroyed
两个生命周期钩子函数中调用,最终调用的是router-view
所渲染vnode节点的数据对象上注册的registerRouteInstance
钩子。 这个钩子有什么意义呢?它的作用就是在router-view
创建的vnode节点最终渲染的实例对象,它被beforeCreate
的时候关联到matched.instances
这个对象上面去,它被destroyed
的时候又从matched.instances
里面移除。
然后是data.hook.prepatch
这个钩子函数,在vue
内部被调用,它是什么场景被调用呢?在vue-router的使用场景中,当路由相同,只是params
不同时,路由组件实例会被重用,而不是重新render
,此时data.hook.prepatch
就会被调用,这个回调函数会传入两个参数,第一个参数是重用前的vnode
,第二参数新创建的vnode
。 在上面的源码中,在prepatch
内部,通过第二个回调参数,更新了matched.instances
。
最后是data.hook.init
这个钩子函数,它的被调用场景是,当路由变化,组件位于keep-alive模式下,并且从inactive
恢复到active
时。 上面的源码中,在init
函数内部,也是更新了matched.instances
。
总结一下,就会看到,上面这三个钩子函数,本质上都做一件事情,就是维护matched.instances
,让matched.instances
始终能够指向的是与当前路由匹配的、实际被用户所看到的vue
实例对象。 为什么要这么做呢?还记得vue-router
那些守卫函数吧,vue-router
是怎么完成那些实例上的守卫函数调用的呢?原因就在这里,就是因为vue-router
总是知道当前渲染的节点是什么,所以在路由切换时,它就能在要离开的以及要渲染进来的节点实例上去调用对应的钩子函数!其它源码会告诉我们更多细节,敬请期待~
(完)。