Vue源码过程(6)- 路由

路由注册

改一下例子

main.js

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
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App'

Vue.use(VueRouter)

// 1. 定义(路由)组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写)相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
el: '#app',
render(h) {
return h(App)
},
router
})

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</template>

<script>
export default {
name: "App",
props: {
},

};
</script>

Vue.use(VueRouter)开始吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function initUse (Vue) {
Vue.use = function (plugin) {
var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if (installedPlugins.indexOf(plugin) > -1) {
return this
}

// additional parameters
var args = toArray(arguments, 1);
args.unshift(this);
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
plugin.apply(null, args);
}
installedPlugins.push(plugin);
return this
};
}

这个方法接受一个 plugin 参数,并且有一个存储所有注册过的 plugin的数组,它会判断plugin是否有install方法,有就执行,然后把plugin添加到installedPlugins数组

走进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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var _Vue;

function install (Vue) {
if (install.installed && _Vue === Vue) { return }
install.installed = true;

_Vue = Vue;

var isDef = function (v) { return v !== undefined; };

var registerInstance = function (vm, callVal) {
var i = vm.$options._parentVnode;
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal);
}
};

Vue.mixin({
beforeCreate: function 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: function destroyed () {
registerInstance(this);
}
});

Object.defineProperty(Vue.prototype, '$router', {
get: function get () { return this._routerRoot._router }
});

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

Vue.component('RouterView', View);
Vue.component('RouterLink', Link);

var strats = Vue.config.optionMergeStrategies;
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

1)installed判断是否安装过

2)Vue.mixinbeforeCreatedestroyed钩子函数注册到组件上

3)把Vue.prototype上的$router$route变成响应式对象

4)Vue.component定义了两个组件

VueRouter

看下创建router实例的时候做了什么

1
2
3
const router = new VueRouter({
routes
})

这个是VueRouter构造函数

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
var VueRouter = function VueRouter (options) {
if ( options === void 0 ) options = {};

this.app = null;
this.apps = [];
this.options = options;
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
this.matcher = createMatcher(options.routes || [], this);

var mode = options.mode || 'hash';
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
if (this.fallback) {
mode = 'hash';
}
if (!inBrowser) {
mode = 'abstract';
}
this.mode = mode;

switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base);
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback);
break
case 'abstract':
this.history = new AbstractHistory(this, options.base);
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, ("invalid mode: " + mode));
}
}
};

1)this.options保留了传入的options参数

2)this.matcher表示路由匹配器

进入createMatcher方法

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
function createMatcher (
routes,
router
) {
var ref = createRouteMap(routes);
var pathList = ref.pathList;
var pathMap = ref.pathMap;
var nameMap = ref.nameMap;

function match (
raw,
currentRoute,
redirectedFrom
) {
var location = normalizeLocation(raw, currentRoute, false, router);
var name = location.name;

if (name) {
var record = nameMap[name];
if (process.env.NODE_ENV !== 'production') {
warn(record, ("Route with name '" + name + "' does not exist"));
}
if (!record) { return _createRoute(null, location) }
var paramNames = record.regex.keys
.filter(function (key) { return !key.optional; })
.map(function (key) { return key.name; });

if (typeof location.params !== 'object') {
location.params = {};
}

if (currentRoute && typeof currentRoute.params === 'object') {
for (var key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key];
}
}
}

location.path = fillParams(record.path, location.params, ("named route \"" + name + "\""));
return _createRoute(record, location, redirectedFrom)
} else if (location.path) {
location.params = {};
for (var i = 0; i < pathList.length; i++) {
var path = pathList[i];
var record$1 = pathMap[path];
if (matchRoute(record$1.regex, location.path, location.params)) {
return _createRoute(record$1, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}

function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap);
}

...
...
return {
match: match,
addRoutes: addRoutes
}
}

首先走进createRouteMap方法,这里是创建一个路由映射表

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
function createRouteMap (
routes,
oldPathList,
oldPathMap,
oldNameMap
) {
// the path list is used to control path matching priority
var pathList = oldPathList || [];
// $flow-disable-line
var pathMap = oldPathMap || Object.create(null);
// $flow-disable-line
var nameMap = oldNameMap || Object.create(null);

routes.forEach(function (route) {
addRouteRecord(pathList, pathMap, nameMap, route);
});

// ensure wildcard routes are always at the end
for (var i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0]);
l--;
i--;
}
}

return {
pathList: pathList,
pathMap: pathMap,
nameMap: nameMap
}
}

走进去addRouteRecord方法

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

function addRouteRecord (
pathList,
pathMap,
nameMap,
route,
parent,
matchAs
) {
var path = route.path;
var name = route.name;
if (process.env.NODE_ENV !== 'production') {
assert(path != null, "\"path\" is required in a route configuration.");
assert(
typeof route.component !== 'string',
"route config \"component\" for path: " + (String(
path || name
)) + " cannot be a " + "string id. Use an actual component instead."
);
}

var pathToRegexpOptions =
route.pathToRegexpOptions || {};
var normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict);

if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive;
}

var record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name: name,
parent: parent,
matchAs: matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
};

if (route.children) {
// Warn if route is named, does not redirect and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
if (process.env.NODE_ENV !== 'production') {
if (
route.name &&
!route.redirect &&
route.children.some(function (child) { return /^\/?$/.test(child.path); })
) {
warn(
false,
"Named Route '" + (route.name) + "' has a default child route. " +
"When navigating to this named route (:to=\"{name: '" + (route.name) + "'\"), " +
"the default child route will not be rendered. Remove the name from " +
"this route and use the name of the default child route for named " +
"links instead."
);
}
}
route.children.forEach(function (child) {
var childMatchAs = matchAs
? cleanPath((matchAs + "/" + (child.path)))
: undefined;
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
});
}

if (!pathMap[record.path]) {
pathList.push(record.path);
pathMap[record.path] = record;
}

if (route.alias !== undefined) {
var aliases = Array.isArray(route.alias) ? route.alias : [route.alias];
for (var i = 0; i < aliases.length; ++i) {
var alias = aliases[i];
if (process.env.NODE_ENV !== 'production' && alias === path) {
warn(
false,
("Found an alias with the same value as the path: \"" + path + "\". You have to remove that alias. It will be ignored in development.")
);
// skip in dev to make it work
continue
}

var aliasRoute = {
path: alias,
children: route.children
};
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
);
}
}

if (name) {
if (!nameMap[name]) {
nameMap[name] = record;
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
"Duplicate named routes definition: " +
"{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
);
}
}
}

最后得到的就是 pathListpathMapnameMap。其中 pathList 是为了记录路由配置中的所有 path,而 pathMapnameMap 都是为了通过 pathname 能快速查到对应的 RouteRecord

3)this.fallback表示根据传入的配置参数,并且浏览器是否支持,初始化不同history实例

这个例子中的history实例

WX20190908-125822@2x

之后返回VueRouter的实例router

new Vue的时候,走进init之后,触发beforeCreate生命周期钩子(在上面做mergeOptions操作(参数合并)的时候,为vm.$options添加(或合并)的钩子函数)

WX20190908-122910@2x

进入调用钩子函数

1
2
3
4
5
6
7
8
9
10
11
beforeCreate: function 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);
}

进入this._router.init(this)

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
VueRouter.prototype.init = function init (app /* Vue component instance */) {
var this$1 = this;

process.env.NODE_ENV !== 'production' && assert(
install.installed,
"not installed. Make sure to call `Vue.use(VueRouter)` " +
"before creating root instance."
);

this.apps.push(app);

// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', function () {
// clean out app from this.apps array once destroyed
var index = this$1.apps.indexOf(app);
if (index > -1) { this$1.apps.splice(index, 1); }
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this$1.app === app) { this$1.app = this$1.apps[0] || null; }
});

// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}

this.app = app;

var history = this.history;

if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation());
} else if (history instanceof HashHistory) {
var setupHashListener = function () {
history.setupListeners();
};
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
);
}

history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});
};

先进入transitionTo方法

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
History.prototype.transitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;

var route = this.router.match(location, this.current);
this.confirmTransition(
route,
function () {
this$1.updateRoute(route);
onComplete && onComplete(route);
this$1.ensureURL();

// fire ready cbs once
if (!this$1.ready) {
this$1.ready = true;
this$1.readyCbs.forEach(function (cb) {
cb(route);
});
}
},
function (err) {
if (onAbort) {
onAbort(err);
}
if (err && !this$1.ready) {
this$1.ready = true;
this$1.readyErrorCbs.forEach(function (cb) {
cb(err);
});
}
}
);
};

1)根据locationthis.current找到匹配到目标的路径

2)进入confirmTransition方法做真正的切换,最后会执行传入的onComplete回调,其实最后调用的是history.setupListeners

WX20190908-180620@2x

WX20190908-180811@2x

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
HashHistory.prototype.setupListeners = function setupListeners () {
var this$1 = this;

var router = this.router;
var expectScroll = router.options.scrollBehavior;
var supportsScroll = supportsPushState && expectScroll;

if (supportsScroll) {
setupScroll();
}

window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
function () {
var current = this$1.current;
if (!ensureSlash()) {
return
}
this$1.transitionTo(getHash(), function (route) {
if (supportsScroll) {
handleScroll(this$1.router, route, current, true);
}
if (!supportsPushState) {
replaceHash(route.fullPath);
}
});
}
);
};

这里会根据浏览器支持和选择,监听popstate或者haschange事件,设置回调

init之后调用Vue中定义的defineReactive_route进行劫持,这个劫持之前有说过,其实是执行的依赖收集的过程,执行_routeget就会对当前的组件进行依赖收集,如果对_route进行重新赋值触发setter就会使收集的组件重新渲染。(完成依赖收集在mount阶段,生成RouterViewvnode的时候收集依赖)

点击router-link组件试一下,执行了router.push(location,noop)

WX20190908-185554@2x

这个location参数在这里,组件render的时候获得的

WX20190908-185859@2x

走进push方法

1
2
3
4
5
6
7
8
9
10
11
12
VueRouter.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;

// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise(function (resolve, reject) {
this$1.history.push(location, resolve, reject);
})
} else {
this.history.push(location, onComplete, onAbort);
}
};

WX20190908-190519@2x

进入this.history.push方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HashHistory.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;

var ref = this;
var fromRoute = ref.current;
this.transitionTo(
location,
function (route) {
pushHash(route.fullPath);
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
},
onAbort
);
};

进入transitionTo方法

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
History.prototype.transitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;

var route = this.router.match(location, this.current);
this.confirmTransition(
route,
function () {
this$1.updateRoute(route);
onComplete && onComplete(route);
this$1.ensureURL();

// fire ready cbs once
if (!this$1.ready) {
this$1.ready = true;
this$1.readyCbs.forEach(function (cb) {
cb(route);
});
}
},
function (err) {
if (onAbort) {
onAbort(err);
}
if (err && !this$1.ready) {
this$1.ready = true;
this$1.readyErrorCbs.forEach(function (cb) {
cb(err);
});
}
}
);
};

获取到跳转的路由对象,进入confirmTransition方法,最后触发传入的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function () {
this$1.updateRoute(route);
onComplete && onComplete(route);
this$1.ensureURL();

// fire ready cbs once
if (!this$1.ready) {
this$1.ready = true;
this$1.readyCbs.forEach(function (cb) {
cb(route);
});
}
},

进入updateRoute方法

1
2
3
4
5
6
7
8
History.prototype.updateRoute = function updateRoute (route) {
var prev = this.current;
this.current = route;
this.cb && this.cb(route);
this.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
};

进入this.cb回调,遍历this.$1.apps,修改_route属性,触发setter,之后触发视图更新

1
2
3
4
5
history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});

这个this.$1.apps是在init的时候往里面push

WX20190908-192258@2x

最后调用回调函数

WX20190908-205722@2x

走进去pushHash

1
2
3
4
5
6
7
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path));
} else {
window.location.hash = path;
}
}

走进pushState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function pushState (url, replace) {
saveScrollPosition();
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
var history = window.history;
try {
if (replace) {
history.replaceState({ key: _key }, '', url);
} else {
_key = genKey();
history.pushState({ key: _key }, '', url);
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url);
}
}

WX20190908-210104@2x

router-link与a标签的区别

router-link组件有自己的render方法,在这里添加了on的属性,之后生成DOM结构的时候就会为a标签添加

WX20191028-222327@2x

a标签点击的时候,进去handler方法

WX20191028-221738@2x

guardEvent判断是否event上有preventDefault,有就调用,阻止默认行为

WX20191028-221608@2x

总结

hash路由,根据情况使用的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var supportsPushState = inBrowser && (function () {
var ua = window.navigator.userAgent;

if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}

return window.history && 'pushState' in window.history
})();

supportsPushState为true的时候:

  • use方法,调用install方法,为所有组件添加beforeCreate和destroyed钩子,还有定义了RouterView和RouterLink组件,并且让\$router和$route变成响应式

  • 创建router实例,根据不同的支持实例化不同的history,还有个match方法匹配地址。

  • new Vue 进入beforeCreate钩子调用,调用transitionTo,根据location匹配出route,调用comfirmTransiton,之后添加监听事件popstate,渲染视图(routerlink渲染)

  • 点击routerlink生成的a标签,触发点击事件,被阻止冒泡,之后调用router.push(location, noop); 进入history.push,又进入了transitionTo,调用dep.notify,之后调用 history.pushState({ key: _key }, ‘’, url); (比如”http://localhost:8080/#/foo")之后更新搜索栏地址。

  • 更新视图

supportsPushState为false的时候:

  • use方法,调用install方法,为所有组件添加beforeCreate和destroyed钩子,还有定义了RouterView和RouterLink组件,并且让\$router和$route变成响应式

  • 创建router实例,根据不同的支持实例化不同的history,还有个match方法匹配地址。

  • new Vue 进入beforeCreate钩子调用,调用transitionTo,根据location匹配出route,调用comfirmTransiton,之后添加监听事件hashchange,渲染视图(routerlink渲染)。

  • 点击routerlink生成的a标签,触发点击事件,被阻止冒泡,之后调用router.push(location, noop); 进入history.push,又进入了transitionTo,调用dep.notify,之后调用pushHash(route.fullPath),因为supportsPushState为false所以执行的是window.location.hash = path(比如path是/foo) ,之后更新搜索栏地址。也触发了hashchange事件
  • 更新视图