My Little World

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
38
39
40
41
42
43
<body>
<div id = 'app' class='hello'>
<p>
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar/user">Go to Bar</router-link>
</p>
<router-view></router-view>
</div>
<script type="text/javascript" src="vue.js"></script>
<script type="text/javascript" src="vue-router.js"></script>
<script>
const Foo = { template: '<div>foo</div>',mounted(){}}//路由foo组件
const Bar = {
template: '<div>bar<div>{{id}}</div><a @click="aa">go to foo</a></div>',//路由bar组件
data(){
return {
id:'',
}
},
methods:{
aa(){
console.log(this.$router)
this.$router.push({name:'foo',params:{name:'jck'}})
}
},
mounted(){
console.log(this.$route,this.$router)
this.id = this.$route.params.id
}
}
const routes = [
{ path: '/foo/:name',name:'foo', component: Foo },
{ path: '/bar/:id?',name:'bar', component: Bar }
]
const router = new VueRouter({routes})
let app = new Vue({
el:'#app',
data:{},
method:{},
router,
})
</script>
</body>

执行vue-Router.js文件

引入vue-router之前会先引入vue.js文件,执行会生成vue构造函数,
因为vue-router的执行过程会用到vue构造函数这个对象上的方法
当代码引入vue-router.js时,里面相应的代码会执行一遍,
此时vue构造函数已经生成
所以vue-router.js运行后,
不仅会生成vue-router的构造对象
还会调用vue构造函数上的use方法将与router相关的信息挂到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
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
(function (global, factory) {
global.VueRouter = factory());
}(this, function () { 'use strict';
//vue-router的构造对象
var VueRouter = function VueRouter (options) {
if ( options === void 0 ) options = {};
//options即new vueRouter时传进来的参数对象,这里只有一个属性即路由配置对象routes
this.app = null;
this.apps = [];
this.options = options;
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
//该函数会根据路由配合生成路由表,返回两个函数,一个match函数用来匹配路由,另一个addRoutes用来动态增加路由,详见下文具体分析
this.matcher = createMatcher(options.routes || [], this);
var mode = options.mode || 'hash';//路由对象模式,没有配置的话默认初始化为'hash'
//fallback参数用于配置当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式
//默认值为 true 官文:https://router.vuejs.org/zh/api/#fallback
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
if (this.fallback) { //history模式下在不支持pushstate方法且配置允许回退hash的情况下回退hash模式
mode = 'hash';
}
if (!inBrowser) { //没有在浏览器中,那应该是在nodejs中,模式重置为abstract
mode = 'abstract';
}
this.mode = mode;//模式确定

switch (mode) { //初始化history对象,三种类型的history构造函数继承自同一父类构造函数History
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:
{
assert(false, ("invalid mode: " + mode));
}
}
};
VueRouter.install = install;
VueRouter.version = '3.1.5';

function install (Vue) { } //挂到VueRouter上,被vue.use函数调用

if (inBrowser && window.Vue) { //只在浏览器环境自动挂载,说明在node.js中需要自己调用Vue.use手动挂载
window.Vue.use(VueRouter);
}

return VueRouter;

}));

vue.use 与 VueRouter.install

vue的use方法会调用插件的install方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//在运行initGlobalAPI(Vue)函数时调用initUse挂载到VUE上
Vue.use = function (plugin) {
var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));//this是指Vue哦
if (installedPlugins.indexOf(plugin) > -1) { //避免重复挂载
return this
}
//这个方法会从arguments中取出从第一个数以后的所有参数,这里返回空数组===>[]
var args = toArray(arguments, 1);
args.unshift(this);//this是vue,将vue传给组件进行初始化 ==>[vue]
if (typeof plugin.install === 'function') { //组件定义了plugin.install方法,执行组件plugin.install方法
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') { //没定义定义plugin.install方法且组件本身是函数,则调用自身
plugin.apply(null, args);
}
installedPlugins.push(plugin);//将插件推入vue对象上的installedPlugins集合中
return this
};

VueRouter.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
50
51
function install (Vue) { //挂到VueRouter上,被vue.use函数调用
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);//如果定义了registerRouteInstance就执行
}
};
//调用vue.mixin方法 将beforeCreate 和 destroyed,两个生命周期函数存到到对应的钩子函数的集合中
//在执行new vue时会调用callHook执行 beforeCreate相关的钩子函数
//在vue中执行destroyed钩子函数时,同样也会调用这里定义好的生命周期函数
Vue.mixin({
beforeCreate: function beforeCreate () { //这里this指向vue
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
//调用vueRouter对象上的init方法,主要功能是初始化当前路由,设置监听 详见下文
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
//defineReactive会对数据进行劫持,进行依赖收集,方便之后页面切换进行监听
} else {//如果没有在new vue时传进vuerouter对象
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed: function destroyed () {
registerInstance(this);
}
});
//访问this.$router,返回new vueRouter生成的对象
Object.defineProperty(Vue.prototype, '$router', {
get: function get () { return this._routerRoot._router }
});
//访问this.$route,当前页面路由对象 this._router.history.current
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
//挂载RouterView,RouterLink两个组件
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);
//使用vue生命钩子函数合并策略定义路由生命周期钩子函数合并策略 其实就是mergeHook函数
var strats = Vue.config.optionMergeStrategies;
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

小结

vueRouter在执行阶段会做以下两件事
1.生成vue-router的构造对象
2.挂载vuerouter相关信息到vue对象

挂载到vue对象时会做以下几件事
1.注入两个生命周期函数:beforeCreate和 destroyed
2.指定vue的$router和$route分别指向传进去的vueRouter对象的本身和history.current属性
3.挂载RouterView,RouterLink两个组件
4.指定路由生命周期函数合并策略使用vue中生命周期函数的合并策略

beforeCreate会在new vue时被调用,主要做以下几件事
1.在vue对象上指定与router相关的属性
2.调用vuerouter的init方法,初始化当前路由,设置监听
3.对当前路由进行数据劫持,进行监听

在new vueRouter时,会做以下几件事
1.根据路由配置数组生成路由表,匹配函数以及动态增加路由的函数
2.确定路由模式
3。根据路由模式创建history属性

一个疑惑

当跳转的路由带有query或者params时,将其中的值插入到页面中为什么不会引起XSS攻击?
因为拿到的值类型都是原始js类型,vue在替换值时只是当做字符串替换,不会进行innerHTML

使用js跳转路由

比如this.$router.push({name:foo,query:{id:1}})方法跳转路由
目的页面拿到的query/params数据,就是调用push函数传入的数据
当我们调用push函数时,就是在调用这段代码

1
2
3
4
5
6
7
8
9
10
VueRouter.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
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);
}
};

location就是我们传入的参数,没有传入 onComplete, onAbort,所以会进入if分支
可见无论进入哪个分支都会调用history对象的push方法
由于在new VueRouter对象时,在构造函数中history是根据传入的选项生成的

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
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:
{
assert(false, ("invalid mode: " + mode));
}
}

所以push方法的实现可能为以下三种情况

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

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

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);//更改url
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
},
onAbort
);
};
AbstractHistory.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
this.transitionTo(
location,
function (route) {
this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route);
this$1.index++;
onComplete && onComplete(route);
},
onAbort
);
};

可见三种模式下都会调用共同的父类History上的transitionTo方法
接下来以HashHistory为基础分析接下来的流程

在HashHistory push调用transitionTo时,
传入三个参数:
location 即我们调用push时传入的参数
onAbort 调用push时没有传入,故该值为undefined
第三个参数是一个匿名函数

1
2
3
4
5
function (route) {
pushHash(route.fullPath);//更改url
handleScroll(this$1.router, route, fromRoute, false);//处理页面滚动相关功能
onComplete && onComplete(route);//调用push时没有传入onComplete,故onComplete=>undefined,不会被执行
},

可见匿名函数内将来要执行pushHash函数

pushHash函数更改浏览器url

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
function pushHash (path) {
if (supportsPushState) { 使用history.pushState更改URl
pushState(getUrl(path));
} else {//如果不支持history.pushState则直接更改hash值
window.location.hash = path;
}
}
function getUrl (path) {
var href = window.location.href;
var i = href.indexOf('#');
var base = i >= 0 ? href.slice(0, i) : href;
return (base + "#" + path)
}
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) { //在使用replace函数跳转路由会走该分支,由replaceState函数调用,replace传入为true
// 保留现有的历史记录状态,因为用户可能会覆盖它
var stateCopy = extend({}, history.state);
stateCopy.key = getStateKey();
history.replaceState(stateCopy, '', url); //使用window的history上的方法
} else { //没有传入replace,故进入该分支
history.pushState({ key: setStateKey(genStateKey()) }, '', url);
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url);
}
}

所以pushHash函数最终的目的是更改浏览器中的url
handleScroll函数处理页面滚动状态,这里暂不做深入
onComplete 值为undefined,故不执行

transitionTo

1
2
3
4
5
6
7
8
9
History.prototype.transitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;
var route = this.router.match(location, this.current);//得到目的路由的route对象
this.confirmTransition(...暂时省略));
};

transitionTo函数中第一步就是根据根据当前路由信息和传入的目的路由信息生成目的路由对象

1
var route = this.router.match(location, this.current);

location 是我们调用push函数传入的参数,
this.current即当前页面路由信息对象

1
2
3
4
5
6
7
8
9
VueRouter.prototype.match = function match (
raw,
current,
redirectedFrom
) {
return this.matcher.match(raw, current, redirectedFrom)
};

this.matcher = createMatcher(options.routes || [], this); //new vuerouter时在构造函数中生成

createMatcher

createMatcher函数即创造匹配器的函数,主要做三件事
1.生成路由表
2.生成匹配器,用于将当然路由对象和目的路由信息对象合成目的路由对象
3.生成动态添加路由的函数,因这里可以直接使用路由表

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
function createMatcher (
routes,
router
) {
var ref = createRouteMap(routes);//根据路由配置生成路由表
var pathList = ref.pathList;
var pathMap = ref.pathMap;
var nameMap = ref.nameMap;

function addRoutes (routes) { //动态增加路由
createRouteMap(routes, pathList, pathMap, nameMap);
}
//根据当前路由对象和目的路由信息以及是否重定向等条件合成目的路由对象返回
function match (
raw,
currentRoute,
redirectedFrom
) {
//统一路由参数,合并params给到目的路由
var location = normalizeLocation(raw, currentRoute, false, router);
var name = location.name;
if (name) {//走这条分支
var record = nameMap[name];
{
warn(record, ("Route with name '" + name + "' does not exist"));
}
if (!record) { return _createRoute(null, location) }
var paramNames = record.regex.keys //没有在路由path中配置动态参数,keys数组为空
.filter(function (key) { return !key.optional; })
.map(function (key) { return key.name; });

if (typeof location.params !== 'object') {
location.params = {}; //给location 挂上params
}

if (currentRoute && typeof currentRoute.params === 'object') {
for (var key in currentRoute.params) {//params中有相同key值时,从当前路由将值给到目的路由
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key];
}
}
}
//如果path有动态参数,使用params将path补全,//以name发起的跳转,挂上了path
location.path = fillParams(record.path, location.params, ("named route \"" + name + "\""));
return _createRoute(record, location, redirectedFrom) 可知返回一route对象
} else if (location.path) { //详见link跳转分析
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)) {
console.log(location)
return _createRoute(record$1, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
function redirect () {...对重定向一系列的处理,会返回createRoute()}
function alias () {...对路由配置中的别名进行处理,会返回createRoute()}
function _createRoute ( //会生成fullpath
record,
location,
redirectedFrom
) {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match: match,
addRoutes: addRoutes
}
}

路由表生成

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
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--;
}
}

{
// warn if routes do not include leading slashes(斜线)
var found = pathList
// check for missing leading slash
.filter(function (path) { return path && path.charAt(0) !== '*' && path.charAt(0) !== '/'; });

if (found.length > 0) {
var pathNames = found.map(function (path) { return ("- " + path); }).join('\n');
warn(false, ("Non-nested routes must include a leading slash character. Fix the following routes: \n" + pathNames));
}
}

return {
pathList: pathList,//数组,存放path值的集合
pathMap: pathMap,//对象,有配置path的路由record集合
nameMap: nameMap//对象,有配置name的路由record的集合
}
}
//根据配置的路由将其转换成内部使用的路由对象record,并根据配置情况放到不同路由集合(前三个参数)中,
function addRouteRecord (
pathList,
pathMap,
nameMap,
route,//具体配置的route
parent,//用于处理配置了children,被递归调用时传入,
matchAs
) {
var path = route.path;
var name = route.name;

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) {...}

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

if (route.alias !== undefined) {...}

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

合并params

在match函数中首先会整理参数

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
function normalizeLocation (
raw,
current,
append,
router
) {
//link标签点击传入的raw是to标签指令的值, 为字符串
var next = typeof raw === 'string' ? { path: raw } : raw;
// named target
if (next._normalized) {//已经处理过
return next
} else if (next.name) { //当前调用push使用name标记路由,只会走这个分支
next = extend({}, raw);
var params = next.params;
if (params && typeof params === 'object') {
next.params = extend({}, params);
}
return next
}

// relative params
if (!next.path && next.params && current) {
next = extend({}, next);
next._normalized = true;
var params$1 = extend(extend({}, current.params), next.params);
if (current.name) {
next.name = current.name;
next.params = params$1;
} else if (current.matched.length) {
var rawPath = current.matched[current.matched.length - 1].path;
next.path = fillParams(rawPath, params$1, ("path " + (current.path)));
} else {
warn(false, "relative params navigation requires a current route.");
}
return next
}

var parsedPath = parsePath(next.path || '');
var basePath = (current && current.path) || '/';
var path = parsedPath.path
? resolvePath(parsedPath.path, basePath, append || next.append)
: basePath;

var query = resolveQuery(
parsedPath.query,
next.query,
router && router.options.parseQuery
);

var hash = next.hash || parsedPath.hash;
if (hash && hash.charAt(0) !== '#') {
hash = "#" + hash;
}
return {
_normalized: true,
path: path,
query: query,
hash: hash
}
}

创建路由route对象

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
function createRoute (
record,
location,
redirectedFrom,
router
) {
var stringifyQuery = router && router.options.stringifyQuery;

var query = location.query || {};
try {
query = clone(query);
} catch (e) {}

var route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query: query,//调用push函数传递进来的query,赋值时只能赋值js数据类型,拿到的也是js数据类型
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),//生成fullPath
matched: record ? formatMatch(record) : []
};
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery);
}
return Object.freeze(route) //禁止改动route对象
}
function getFullPath ( //根据query生成fullPath
ref,
_stringifyQuery
) {
var path = ref.path;
var query = ref.query; if ( query === void 0 ) query = {};
var hash = ref.hash; if ( hash === void 0 ) hash = '';

var stringify = _stringifyQuery || stringifyQuery;
return (path || '/') + stringify(query) + hash
}
//组装url上参数的部分
function stringifyQuery (obj) {
var res = obj ? Object.keys(obj).map(function (key) {
var val = obj[key];

if (val === undefined) {
return ''
}

if (val === null) {
return encode(key)
}

if (Array.isArray(val)) {
var result = [];
val.forEach(function (val2) {
if (val2 === undefined) {
return
}
if (val2 === null) {
result.push(encode(key));
} else {
result.push(encode(key) + '=' + encode(val2));
}
});
return result.join('&')
}

return encode(key) + '=' + encode(val)
}).filter(function (x) { return x.length > 0; }).join('&') : null;
return res ? ("?" + res) : ''
}

所以在调用完match之后会得到route,接着会调用confirmTransition

confirmTransition

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
在transitionTo调用时
this.confirmTransition(
route,
function () {
this$1.updateRoute(route);//更新router对象
onComplete && onComplete(route);//调用transitionTo时传入的第二个参数函数,里面有pushHash来更改url
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);
});
}
}
);

History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
var this$1 = this;

var current = this.current;
var abort = function (err) {
// after merging https://github.com/vuejs/vue-router/pull/2771 we
// When the user navigates through history through back/forward buttons
// we do not want to throw the error. We only throw it if directly calling
// push/replace. That's why it's not included in isError
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
if (this$1.errorCbs.length) {
this$1.errorCbs.forEach(function (cb) {
cb(err);
});
} else {
warn(false, 'uncaught error during route navigation:');
console.error(err);
}
}
onAbort && onAbort(err);
};
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL();
return abort(new NavigationDuplicated(route))
}

var ref = resolveQueue(
this.current.matched,
route.matched
);
var updated = ref.updated;
var deactivated = ref.deactivated;
var activated = ref.activated;

var queue = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(function (m) { return m.beforeEnter; }),
// async components
resolveAsyncComponents(activated)
);

this.pending = route;
var iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort()
}
try {
hook(route, current, function (to) {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this$1.ensureURL(true);
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort();
if (typeof to === 'object' && to.replace) {
this$1.replace(to);
} else {
this$1.push(to);
}
} else {
// confirm transition and pass on the value
next(to);
}
});
} catch (e) {
abort(e);
}
};

runQueue(queue, iterator, function () {
var postEnterCbs = [];
var isValid = function () { return this$1.current === route; };
// wait until async components are resolved before
// extracting in-component enter guards
var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
var queue = enterGuards.concat(this$1.router.resolveHooks);
runQueue(queue, iterator, function () {
if (this$1.pending !== route) {
return abort()
}
this$1.pending = null;
onComplete(route);//所有需要运行的钩子函数运行完执行更改url,更新current,处理页面滚动
if (this$1.router.app) {
this$1.router.app.$nextTick(function () {
postEnterCbs.forEach(function (cb) {
cb();
});
});
}
});
});
};
function runQueue (queue, fn, cb) {
var step = function (index) {
if (index >= queue.length) {
cb();
} else {
if (queue[index]) {
fn(queue[index], function () {
step(index + 1);
});
} else {
step(index + 1);
}
}
};
step(0);
}
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
History.prototype.updateRoute = function updateRoute (route) {
var prev = this.current;
this.current = route;
this.cb && this.cb(route); //通知app更改_route属性值,即更改$route值
this.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
};
this.cb在install的时候被定义如果获取
History.prototype.listen = function listen (cb) {
this.cb = cb;
};
//在new vue时被定义
VueRouter.prototype.init = function init (app ) {
history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});
}
//install的时候
function install (Vue) {
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
}

link标签跳转路由

点击link标签时,根据绑定函数即可知道,同样会调用router.push

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
var router = this.$router;
var current = this.$route;
var ref = router.resolve(
this.to,
current,
this.append
);

var location = ref.location;
var route = ref.route;
var href = ref.href;

var handler = function (e) {
if (guardEvent(e)) {
if (this$1.replace) {
router.replace(location, noop);
} else {
router.push(location, noop);//push,直接走同步流程
}
}
};

var on = { click: guardEvent };
if (Array.isArray(this.event)) {
this.event.forEach(function (e) {
on[e] = handler;
});
} else {
on[this.event] = handler;
}

其中,经过resolve处理过的to标签属性的值,会生成统一规范的后续需要使用的location,route

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
VueRouter.prototype.resolve = function resolve (
to,
current,
append
) {
current = current || this.history.current;
var location = normalizeLocation(
to,
current,
append,
this
);
var route = this.match(location, current);
var fullPath = route.redirectedFrom || route.fullPath;
var base = this.history.base;
var href = createHref(base, fullPath, this.mode);
return {
location: location,
route: route,
href: href,
// for backwards compat
normalizedTo: location,
resolved: route
}
};

location 会在normalizeLocation中以第三个return的位置返回如下结构

1
2
3
4
5
6
{
_normalized: true,
path: path,
query: query,
hash: hash
}

这样相当于提前normalizeLocation调用,所以在match再调用的时候直接返回自身
在接下来的match流程中,会进入以path为依据的流程

1
2
3
4
5
6
7
8
9
10
else if (location.path) {
location.params = {};
for (var i = 0; i < pathList.length; i++) {
var path = pathList[i];
var record$1 = pathMap[path];//根据path拿到record对象
if (matchRoute(record$1.regex, location.path, location.params)) {
return _createRoute(record$1, location, redirectedFrom)
}
}
}

根据标签to赋予的path值,和生成路由表时拿到的path配置值,根据正则表达式解析params对象,
也说明了,为什么跳转路包含动态参数的path时,要跟params配对使用

matchRoute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function matchRoute (
regex,
path,
params
) {
var m = path.match(regex);
if (!m) {//当前路由不符合route定义时格式
return false
} else if (!params) {
return true
}
//匹配路由组装params
for (var i = 1, len = m.length; i < len; ++i) {
var key = regex.keys[i - 1];
var val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i];//始终为子字符串
if (key) {
// Fix #1994: using * with props: true generates a param named 0
params[key.name || 'pathMatch'] = val;
}
}

return true
}

直接更改url跳转

在浏览器直接更改含有动态路由的的url,回车跳转,会根据事先绑定的监听事件进行解析处理
在new 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
25
26
  VueRouter.prototype.init = function init (app /* Vue component instance */) {
var this$1 = this;

this.app = app;

var history = this.history;

if (history instanceof HTML5History) { //h5history在new h5history时,在构造函数中设置popstate事件监听
history.transitionTo(history.getCurrentLocation());
} else if (history instanceof HashHistory) {
var setupHashListener = function () {
history.setupListeners();//设置监听
};
history.transitionTo( //无论成功与否都绑定
history.getCurrentLocation(),
setupHashListener,
setupHashListener
);
}

history.listen(function (route) { //route改变时调用,this.cb
this$1.apps.forEach(function (app) {
app._route = route;
});
});
};

设置监听具体内容

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
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
}
//直接调用transitionTo进行路由更新
this$1.transitionTo(getHash(), function (route) { //成功回调不用pushHash更新url
if (supportsScroll) {
handleScroll(this$1.router, route, current, true);
}
if (!supportsPushState) {
replaceHash(route.fullPath);
}
});
}
);
};

可见通过监听事件触发的路由改变会直接调用transitionTo函数进行跳转

两个收获

1.normalizeLocation函数发挥的作用,根据第一参数location对象不同的属性状态进行不同的处理,用于在上游的不同情境(不同方式导致跳转)使用中统一调用
2.在transitionTo函数中调用comfirmTransition,为什么不直接在transitionTo函数中书写confirmTransition函数内容呢,为什么要单独摘出来呢?同样,comfirmTransition函数不止会在transition中调用,
还会在AbstractHistory的go函数中被直接调用,HashHistory和HTML5History的go方法会直接使用window.history.go(n);nodejs全局对象是global,不是windows,所以需要特殊处理

vuerouter