vue-router.js源码学习 —— 初识Vue Router

在开发复杂的单页应用时一般都会使用到Vue Router,Vue Router 使得开发 Web 应用的体验和开发原生应用很像,原生应用的系统一般都会自带对页面栈管理的对象,可以方便地在页面之间跳转,却不用重新刷新页面。

今天这篇文章我们先来了解一下 Vue Router,会主要分析它的install函数,VueRouter类,以及<router-linker><router-view>的实现过程。内容有点多,文章稍长,不过把这些都搞明白后,熟练使用 Vue Router 应该没有问题。

准备工作

一边调试源码一边分析的效率会高很多,所以我们先看一下如何通过自己写的例子调试到源代码。

将项目跑起来

  1. 将 Vue Router 的源码 clone 下来:

    1
    git clone https://github.com/vuejs/vue-router.git --depth 1

    这个项目的 tag 太大了,所以指定--depth 1,否则容易拉取失败。

  2. 了解项目的目录结构,主要的几个如下:

    • build:打包 Vue Router 的脚本和配置文件,可以看到跟 Vue 一样也是使用的rollup打包的。

    • dist:打包后的文件,分了很多各版本,在build/config.js中可以找对应的是什么版本。

    • examples:官方给的演示的例子,这些例子是使用webpack打包的,因为里面用到import导入模块,也用到了 Vue 的模板文件,所以直接在浏览器中访问这里的html是没有用的,也需要启动服务。

    • flow:项目中的类型声明,所以跟 Vue 一样,Vue Router 也是使用flow做类型注解的。

    • src:Vue Router 的源码,我们后面重点分析的地方。components中是 Vue Router 提供的两个组件<router-link><router-view>的实现。history中放的是 Vue Router 提供的几种路由方式的实现。

    • test:项目的测试用例,在遇到想不明白的代码的地方可以找找这里,说不定有对那个点的测试用例。

  3. 运行项目

    1
    2
    npm install
    npm run dev

    在浏览器访问http://localhost:8080/,会看到所有examples文件夹下的例子。

准备例子

example文件夹中再创建一个文件debug-start,然后创建index.htmlapp.js

1
2
3
4
5
6
7
8
9
10
11
<!-- index.html -->

<!DOCTYPE html>
<link rel="stylesheet" href="/global.css">
<a href="/">&larr; Examples index</a>
<button id="unmount">Unmount</button>
<hr />
<div id="app"></div>
<script src="/__build__/shared.chunk.js"></script>
<!-- 注意这里要用 debug-start.js -->
<script src="/__build__/debug-start.js"></script>
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
// app.js

import Vue from 'vue'
import VueRouter from 'vue-router'

// 1. Use plugin
Vue.use(VueRouter)

// 2. Define router components
const Home = { template: '<div>home</div>'}
const Foo = { template: '<div>foo</div>'}
const Bar = { template: '<div>bar</div>'}

// 3. Create the router
const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
})

// 4. Create and mount root instance
const vueInstance = new Vue({
router,
data: () => ({ n: 0 }),
methods: {
to (path) {
this.$router.push(path)
}
},
template: `
<div id="app">
<h1>Debug Start</h1>
<ul>
<li v-on:click="to('/')">/</li>
<li v-on:click="to('/foo')">/foo</li>
<li v-on:click="to('/bar')">/bar</li>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')

打开example/index.html,然后最下面加上:

1
<li><a href="debug-start">Debug Start</a></li>

这样在服务启动后就可以通过首页上的链接跳转过去了。打开控制台可以找到 Vue Router 的源码,之后打断点调试就可以了。

install

install函数

在上一篇文章Vue.js源码学习 —— 各全局API的实现中讲了Vue.use的时候,也顺便说了一下一般可以给 Vue 添加全局功能的方式,正好我们这里看一下 Vue Router 作为 Vue 的插件,都给 Vue 添加了哪些功能。

这部分的代码在文件src/install.js中,我把install的主要构成抽出来如下:

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
export function install (Vue) {
// ...

// 注入组件选项 beforeCreate destroyed
Vue.mixin({
beforeCreate () {
// ...
},
destroyed () {
// ...
}
})

// 添加实例方法
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})

Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})

// 注册两个全局的组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)

// 设置 Vue Router 的钩子函数的合并策略和 Vue实例生命周期钩子 created 的一样
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

可以看到 Vue Router hook了两个 Vue 实例生命周期中的钩子方法beforeCreatedestroyed,这意味着后面创建的 Vue 实例在beforeCreatedestroyed被触发时,都会先执行这里的两个钩子方法。然后给 Vue实例增加了$router$route两个方法,这也是我们平时在开发中用到的比较多的两个方法。它们都来自this._routerRoot,说明在beforeCreate方法中为Vue实例添加了_routerRoot对象,我们等会看一下。最后注册了两个全局组件<router-view><router-link>,也是在开发中会经常用到的。

接下来重点看一下beforeCreate方法做的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
},
// ...
})

首先判断了选项是否有router,在上面的例子中在创建 Vue 实例时我们传了router选项:

1
2
3
4
const vueInstance = new Vue({
router,
// ...
)}

此时给 Vue 实例添加了_routerRoot属性,并且指向该 Vue 实例。然后给 Vue 实例又添加了_router属性,这个值就是传入的router选项,也是例子中创建的VueRouter实例对象。接着用 Vue 实例来初始化router,所以在创建router时还没有调用init方法。最后又给 Vue 实例加上了_route属性,注意,这里调用了Vue.util.defineReactive_route转变为响应式的,其值是_router.history.current,也就是当前路由中的信息。

如果 Vue 实例的选项中没有router,将_routerRoot指向该实例的父实例,父实例没有的话,还是指向自己,但是其他的事情不做了。

经过beforeCreate方法后,我们来看一下现在 Vue 实例多出来的几个属性的内容:

所以可以看到vm.$router是对整个路由的管理者,而vm.$route只是当前页面的路由信息,这回在开发中应该不会弄混了吧~

registerInstance函数中主要用到的是registerRouteInstance方法,这个是在实现<router-view>时才有的,下一篇介绍组件的实现时再展开看。

作为插件使用

这里只看到了install函数,可是在Vue.use中使用的并不是它,而是VueRouter

1
2
// 1. Use plugin
Vue.use(VueRouter)

那就看一下VueRouter,代码位于src/index.js中。我把与安装插件相关的代码摘出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { install } from './install'
// ...

export default class VueRouter {
static install: () => void
// ...
}

VueRouter.install = install
// ...
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}

可以看到,是将上面看到的install函数作为了VueRouter类的静态方法。在分析Vue.use的实现时我们已经知道,一个插件要么提供install方法,要么自身是一个函数。

同时注意到,在检测到 Vue 是可访问的全局变量时会自动调用Vue.use(),这是开发者不显示调用Vue.use也可以。

VueRouter

VueRouter类中封装的大部分API都是对外提供给开发者使用的,可以在官方API文档中查看它们的作用。

VueRouter构造函数

VueRouter 构造函数接收一个options参数,类型是RouterOptions,到flow中看一下options都可以是哪些参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare type RouterOptions = {
routes?: Array<RouteConfig>;
mode?: string;
fallback?: boolean;
base?: string;
linkActiveClass?: string;
linkExactActiveClass?: string;
parseQuery?: (query: string) => Object;
stringifyQuery?: (query: Object) => string;
scrollBehavior?: (
to: Route,
from: Route,
savedPosition: ?Position
) => PositionResult | Promise<PositionResult>;
}
  • routes:是一个RouteConfig类型的数组,每一个RouteConfig就是一个route对象。

  • mode:路由使用的模式,可选值为hashhistoryabstract

  • fallback:当浏览器不支持history.pushState控制路由是否应该退回到hash模式。默认值为true

  • base:应用所在的根路径,比如上面的例子中,base就是debug-start,这样页面的路由都会是http://localhost:8080/debug-start加上path中定义的路径。

  • linkActiveClass:全局配置<router-link>默认的激活的class,默认提供了router-link-active

  • linkExactActiveClass:全局配置<router-link>默认的精确激活的class,默认提供了router-link-exact-active

  • parseQuery/stringifyQuery:提供自定义查询字符串的解析/反解析函数。覆盖默认行为。

  • scrollBehavior:自定义路由切换时页面如何滚动。

这些字段的用法在官方API文档中都可以查到,后面我们会看到这些选项具体是怎么用的。

再来看一下构造函数中主要做的事:

  • 初始化需要的变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 在调用 init 时传过来的 Vue 实例,也就是路由的起始点,Vue 根实例
    this.app = null
    // 因为 VueRouter 是注册在全局的,所以有可能有多个app
    this.apps = []
    // 上面说的构造函数中传入的选项
    this.options = options
    // 存储钩子方法,可以看到 Router 的钩子分 before、resolve、after这几类
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // 这个matcher到后面再详细分析它的作用
    this.matcher = createMatcher(options.routes || [], this)
  • 确定路由使用的模式。如果开发者在选项中没有指定mode,默认使用hash。如果指定的是history,但是当前运行环境不支持history.pushState,并且开发者没有指定fallback选项为false,那在浏览器环境中mode退回到hash,非浏览器环境退回到abstract

  • 创建history对象。实际路由的工作是由history完成的。可以看到,每种mode对应了一个**History类。后面我们也会分析。

init

init方法在前面我们看到了它使用的地方,在 Vue 实例的beforeCreate钩子中。所以在没有使用Vue.use(VueRouter)安装之前调用开发环境下会给出告警信息。init的函数签名如下:

1
2
3
init (app: any /* Vue component instance */) {
// ...
}

它的作用就是将app方法apps中,然后设置了history对象的监听器。

实例方法

我这里大概整理一下VueRouter提供的实例方法,在官方API文档中都可以查到。

全局导航守卫

这几个API的函数签名如下:

1
2
3
router.beforeEach((to, from, next) => {})
router.beforeResolve((to, from, next) => {})
router.afterEach((to, from) => {})

to是要前往的route对象,from是即将要离开的route对象。next是一个函数,在beforeEachbeforeResolve中一定要调用next,执行效果取决于给next的传参。

  • next():进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
  • next(false):中断当前的导航。
  • next('/') 或者 next({ path: '/' }):中断当前的导航,跳转到一个新的导航。
  • next(error):导航被终止且该错误会被传递给router.onError()注册过的回调。

导航方法

这几个API的函数签名如下:

1
2
3
4
5
6
7
router.push (location, onComplete?, onAbort?)
router.push (location).then(onComplete).catch(onAbort)
router.replace (location, onComplete?, onAbort?)
router.replace (location).then(onComplete).catch(onAbort)
router.go (n)
router.back ()
router.forward ()

其他方法

1
const matchedComponents: Array<Component> = router.getMatchedComponents (to?)

返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用。

1
2
3
4
5
6
7
8
const resolved: {
location: Location,
route: Route,
href: string,
// for backwards compat
normalizedTo: Location,
resolved: Route
} = router.resolve (to, current?, append?)

解析目标位置。

1
router.addRoutes (routes: Array<RouteConfig>)

动态添加更多的路由规则。参数必须是一个符合 routes 选项要求的数组。

1
router.onReady (cb, errorCb?)

该方法把一个回调排队,在路由完成初始导航时调用。

1
router.onError (errorCb)

注册一个回调,该回调会在路由导航过程中出错时被调用。

从这些方法的实现上可以看出几乎都是对history对象的封装,下一篇文章会主要看history部分的代码,到时再来理解这些方法就容易了。

配置routes

上面看到在创建VueRouter时传入routesRouteConfig类型的数组,查看flow/declarations.js可知RouteConfig的定义如下:

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
declare type RouteConfig = {
// 对应当前路由的路径,总是解析为绝对路径,如“/foo/bar”
path: string;
// 当前路由的名称
name?: string;
// 路由对应的组件
component?: any;
// 路由对应的命名组件
components?: Dictionary<any>;
// 指定从 path 重定向到的路由
redirect?: RedirectOption;
// 给 path 起一个别名
alias?: string | Array<string>;
// 用来配置嵌套路由
children?: Array<RouteConfig>;
// 路由独享的守卫
beforeEnter?: NavigationGuard;
// 路由的元信息,可以自定一些信息在后面需要的查询
meta?: any;
// 通过在路由中配置 props,可以给组件的 props 传值
props?: boolean | Object | Function;
// 匹配规则是否大小写敏感?(默认值:false)
caseSensitive?: boolean;
// 编译正则的选项
pathToRegexpOptions?: PathToRegexpOptions;
}

这些字段的应用场景,在官方文档中都能找到,鉴于文章篇幅我就不一一举例了。

可以看到<router-link>组件提供了3个选项:

1
2
3
4
5
6
7
8
9
export default {
name: 'RouterLink',
props: {
// ...
},
render (h: Function) {
// ...
}
}

我们先来看render函数的实现,然后再总结props中每一项的用途,这样印象会深一些。

render函数

render函数中主要做了这么几件事:

  • 获取需要的变量:

    1
    2
    3
    4
    5
    6
    7
    const router = this.$router
    const current = this.$route
    const { location, route, href } = router.resolve(
    this.to,
    current,
    this.append
    )

    这里看到了对router.resolve的使用,toappend都是给<router-link>传的值。我们举例来看一下:

    1
    2
    3
    4
    <ul>
    <li><router-link to="/foo">/foo</router-link></li>
    <li><router-link :to="{ path: 'bar' }" append>/bar</router-link></li>
    </ul>

    当从/foo切换到/bar时,在导航栏中看到的路径会从/foo变为/foo/bar。注意,path后面一定提供的是要追加的部分,比如上面改为{ path: '/bar' },那路径就直接从/foo变为/bar了。

    router.resolve中获取到的几个变量的内容如下图所示:

  • 获取链接激活时用的CSS样式,因为我们的例子中没有提供activeClassexactActiveClass,也没有提供全局的这两个样式,所以得到的activeClass为默认值router-link-activeexactActiveClass为默认值router-link-exact-active。同时判断了链接当前是否处于激活状态,并将结果存在了classes中。我们的例子中现在classes的内容如下:

    1
    2
    3
    4
    {
    'router-link-active': false,
    'router-link-exact-active': false
    }
  • 获取ariaCurrentValue的值,这主要是给a标签用的aria-current属性用的。设置这个值会对 Screen Readers 比较友好。

    1
    const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null
  • 获取事件监听器放在on中,这个on最终会传给data用于创建组件节点。先提供了一个默认的click事件,然后再将用户传入的event放入on中,我们看一下handler的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const handler = e => {
    if (guardEvent(e)) {
    if (this.replace) {
    router.replace(location, noop)
    } else {
    router.push(location, noop)
    }
    }
    }

    guardEvent函数主要为了确保当前点击的链接可以跳转。接着replace是外部传给组件的,是true的话就会调用router.replace进行跳转,此时history栈中不会留下记录,而router.push是会的。

    举个例子:

    1
    2
    3
    4
    <ul>
    <li><router-link to="/foo" replace>/foo (replace)</router-link></li>
    <li><router-link to="/bar">/bar</router-link></li>
    </ul>

    比如现在在/bar,然后点了/foo,因为/foo设置了replacetrue,所以现在点击浏览器的back按钮时,回不到/bar了。

    此外,从代码中可以看到event也可以传一个数组的,比如下面这样:

    1
    <li><router-link to="/bar" :event="['mousedown', 'touchstart']">/bar</router-link></li>

    现在得到的on的内容如下:

    1
    2
    3
    4
    5
    {
    click: ƒ guardEvent(e),
    mousedown: ƒ handler(e),
    touchstart: ƒ handler(e)
    }
  • 判断用户是否使用了作用域插槽,如果是的话直接返回插槽内容即可。同时还会向外提供几个变量:

    1
    2
    3
    4
    5
    6
    7
    {
    href, // 解析后的URL
    route, // 解析后的规范化的地址
    navigate: handler, // 触发导航的函数,可以看到也是用的 handler
    isActive: classes[activeClass], // 如果需要应用激活时的 class,则为 true
    isExactActive: classes[exactActiveClass] // 如果需要应用精确激活时的 class,则为 true
    }

    举个例子看一下使用插槽的情况:

    1
    2
    3
    4
    5
    <router-link to="/foo" v-slot="props">
    <li :class="[props.isActive && 'active', props.isExactActive && 'exact-active']">
    <a :href="props.href" @click="props.navigate">{{ props.route.path }} (with v-slot).</a>
    </li>
    </router-link>

    咱们来看一下下面这句代码的意思:

    1
    return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)

    如果<router-link>中包裹的元素不是单独一个元素,而是多个的时候,会将这些子元素包裹span标签内。

  • 没有使用插槽的情况下,默认创建的节点的taga,也就是创建一个a标签元素。此时创建节点的代码如下:

    1
    2
    3
    4
    5
    6
    const data: any = { class: classes }

    data.on = on
    data.attrs = { href, 'aria-current': ariaCurrentValue }

    return h(this.tag, data, this.$slots.default)
  • 如果外部传了tag,会获取<router-link>包裹的内容中的第一个a节点,然后将该a节点的onattrs与之前获得的onattrs进行合并。事件监听器会合并成一个数组。举个自定义标签的例子:

    1
    2
    3
    <router-link tag="li" to="/bar" :event="['mousedown', 'touchstart']">
    jump to: <a>/bar</a>
    </router-link>

render函数中做的事情就这些了,下面总结一下props中每个字段的用途,其实从上面的分析中我们已经了解到了。

props

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
props: {
// 目标路由的链接,这个值可以一个字符串也可以是描述目标位置的对象。是必须传的。
to: {
type: toTypes,
required: true
},
// 指定将<router-link>渲染成什么标签,默认是 a 标签。
tag: {
type: String,
default: 'a'
},
// “是否激活”默认类名的依据是包含匹配。
exact: Boolean,
// 设置 append 后会在当前路径后添加 to 中指定的路径,这个路径前面不要加 /。
append: Boolean,
// 设置 replace 后,当点击时会使用 router.relace,而不是 router.push。
replace: Boolean,
// 链接激活时使用的 CSS 类名。
activeClass: String,
// 配置当链接被精准匹配时使用的 CSS 类名。
exactActiveClass: String,
// 当链接根据精确匹配规则激活时配置的 aria-current 的值,默认是 page。
ariaCurrentValue: {
type: String,
default: 'page'
},
// 可以用来触发导航的事件,可以是是一个字符串也可以是一个字符串数组,默认值是 click。
event: {
type: eventTypes,
default: 'click'
}
}

router-view

还是先看一下<router-view>组件提供的选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// ...
}
}

可以看到这是一个函数式组件。并且props只有一个属性name,默认值为default,下面我们就先看一下name的作用,然后再分析render函数的实现。

name属性

<router-view>组件主要用来展示当前路由对应的组件,比如在我们上面的例子中当切换路由时<router-view>的位置也会跟着换。

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
// 定义组件内容
const Home = { template: '<div>home</div>'}
const Foo = { template: '<div>foo</div>'}
const Bar = { template: '<div>bar</div>'}

// 配置每个路径对应一个组件
const router = new VueRouter({
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
})

new Vue({
router,
template: `
<div id="app">
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
</ul>
<router-view></router-view>
</div>
`
}).$mount('#app')

那实际上routes中配置的组件和<router-view>是通过<router-view>name属性匹配上的,上面例子中的<router-view>组件现在的名字是default,那routes中配置的组件也可以调整一下。从前面RouteConfig我们已经知道route对象还有一个components属性可用来配置命名视图。现在修改一下上面的例子:

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
const router = new VueRouter({
routes: [
{ path: '/',
components: {
default: Home
}
},
{ path: '/foo',
components: {
default: Foo
}
},
{ path: '/bar',
components: {
default: Bar
}
}
]
})

new Vue({
router,
template: `
<div id="app">
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
</ul>
<router-view name="default"></router-view>
</div>
`
}).$mount('#app')

这样一样能正常展示,每切换一个组件时,就会去匹配名为default<router-view>组件。那我们也可以给<router-view>组件取一个别的名字,再改一下上面的例子,让foobar一起展示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const router = new VueRouter({
routes: [
{ path: '/',
components: {
default: Home
}
},
{ path: '/views',
components: { // components接受多个键值对
foo: Foo,
bar: Bar
}
}
]
})
1
2
3
4
5
6
7
8
9
<div id="app">
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/views">/views</router-link></li>
</ul>
<router-view name="default"></router-view>
<router-view name="foo"></router-view>
<router-view name="bar"></router-view>
</div>

切换到/views时就去找名字为foobar<router-view>,然后将其替换成对应的组件内容。

到这里相信你对name的作用已经有了一定的了解,对<router-view>的作用应该也了解了,接下来咱们看一下它是如何实现的。

render函数

1
2
3
4
5
6
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
</ul>
<router-view></router-view>
</div>
1
2
3
4
5
const Home = { template: '<div>home</div>' }

routes: [
{ path: '/', component: Home }
]

我们先看像上面这种最简单的情况,render函数中的主要流程,我将其抽出来如下:

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
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
const cache = parent._routerViewCache || (parent._routerViewCache = {})

// 我们的例子中不存在嵌套路由,所以 depth 为 0
let depth = 0

// 从 matched 中取出 component,matched 中存放的是解析好的路由对应的组件
// 数组的索引就是嵌套的深度
const matched = route.matched[depth]
const component = matched && matched.components[name]

// 没有匹配到路由或者没有配置组件,直接返回空节点
if (!matched || !component) {
cache[name] = null
return h()
}

// cache component
cache[name] = { 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
;(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
}
}

// 创建组件的节点
return h(component, data, children)
}

这里使用的是parent.$createElement,并不是给当前render函数传的参数h,注释中说是为了被<router-view>渲染的组件能够解析具名插槽,但是我还没有找到对应的例子,之后遇到了我再补充到这里。

data.registerRouteInstancesrc/index.js中有看到调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}
})

也就是在组件实例初始化时就会将组件实例存入matched.instances,在组件被销毁时会将matched.instances中对应的组件实例置为undefined

此外我们看到还在提供了data.hook中的prepatchinit方法,在其中重新赋值了组件实例。在分析Vue的源码时,我们已经知道这两个钩子会在patch的过程中被触发。

再来看一下对嵌套路由的处理,先举个例子:

1
2
3
4
5
6
7
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/foo">Foo</router-link></li>
</ul>
<router-view></router-view>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Home 中还有一个 <router-view>
const Home = {
template: `   
<div>
<h1>Welcome!</h1>
<router-view></router-view>
</div>
`
}
const Foo = { template: '<div>Foo</div>' }

const router = new VueRouter({
routes: [
{
path: '/',
component: Home,
children: [
// Home 中的 <router-view> 会被渲染为 Foo
{ path: 'foo', component: Foo }
]
}
]
})

此时下面这段代码就起作用了:

1
2
3
4
5
6
7
8
let depth = 0
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
parent = parent.$parent
}

渲染 Foo 时,depth为 1。此时route.matched中的内容如下,Foo 组件是 index 为 1 的项:

同时在上面的循环中也有获取了inactive变量,处理keep-alive的情况,我也是没有知道例子,等后面遇到了再补上-.-

最后再来看一下路由中配置了props的情况的处理,还是举一个简单的例子看一下通过路由给组件传参的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Home = {
props: [ 'userId' ],
template: `  
<div>
<h1>Welcome!</h1>
<p> User Id: {{ userId }}</p>
</div>
`
}

const router = new VueRouter({
base: __dirname,
routes: [
{
path: '/',
component: Home,
props: { userId: '10' } // 当渲染 Home 时,Home 组件可以接收到传过去的 userId 的值
}
]
})

对应下面这段代码:

1
2
3
4
5
6
7
8
9
const configProps = matched.props && matched.props[name]
// save route and configProps in cache
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}

configProps得到的就是{ userId: 10 }。先是给cache[name]中增加了routeconfigProps键值对,此时cache[name]中有3个字段,分别是componentconfigPropsroute

fillPropsinData函数中做的事情是将configProps中的内容分别给到data.propsdata.attrs,如果组件的props中有该字段,就将该字段对应的键值对给data.props,否则就放到data.attrs中。这样就将路由中配置的值传给组件了。

此外,注意到resolveProps函数,可以看出配置在路由中的props以下几种情况:

  • 对象,此时直接使用该对象。
  • 函数,会去执行这个函数,并把route当参数传过去。
  • 布尔值,如果是true,会使用route.params

<router-view>的实现就分析到这里了,可以看其实就是通过<router-view>渲染了一个与路由匹配的组件。

通过这篇文章相信你会对 Vue Router 的使用有个更全面的了解。下一篇文章我们将看一下路由导航究竟是如何实现的,也就是src/history文件夹下的内容。还有就是路由是如何去匹配组件的,也就是matcher部分的代码。

本文作者:意林
本文链接:http://shinancao.cn/2020/03/10/Vue-Source-Code-17/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!