Vue源码分析之 Vue-router 篇
Vue-router 是什么?
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。它支持 hash、history、abstract 3 种路由方式,提供了
Vue-router 初始化
Vue.use(VueRouter) 实质上是启动了 VueRouter 的install方法 安装到vue.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function 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
};
}
可以很清晰的看到,这段代码逻辑,首先会判断 Vue installedPlugins 是否存在,然后执行插件的安装方法,接着将插件存储到 _installedPlugins 中。这个方法的好处是合理利用了this让插件快速找到主体。
Vue-router 安装
在项目中 import VueRouter from ‘vue-router’ 的时候,实际上引用的是一个对象,它的定义在/node_modules/vue-router,在文件夹下很容易就能找到安装方法,这里我采用debugger的模式去循循渐进刨析源码: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
47function 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;
}
这里为确保路由只注册一次,用了installed变量作为记号。这里最重要的一点就是使用Vue.mixin混淆 beforeCreate 和 destroyed 钩子函数注入到每一个组件中。1
2
3
4
5
6function initMixin$1 (Vue) {
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin);
return this
};
}
通过 debugger 我们可以发现,这一步 主要是通过mergeOptions函数将 mixin 中的beforeCreate 和 destroyed 钩子函数合并到vue.options。debugger跳回Vue-Router 的 install 方法,可以看到混入的
beforeCreate 钩子函数 主要是定义了this._routerRoot 表示当前vue实例,this._router 表示 VueRouter 的实例 router,它是在new Vue 时传入的。还执行了 this._router.init() 方法初始化 router。defineReactive 方法 是把this._route 变成响应式对象,刚刚有提到this._routerRoot 表示当前vue实例,所以在执行该钩子函数时this._routerRoot 始终指向的最后一个传入了 router 对象的父实例。
接着给 Vue 原型上定义了 $router 和 $route 2 个属性的 get 方法,这就是为什么我们可以在组件实例上可以访问 this.$router 以及 this.$route。
接着又通过 Vue.component 方法定义了全局的
路径切换
接下来我们看看 vue-router是如何使用的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24VueRouter.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);
}
};
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);
};
通过 debugger 我们可以发现,实则 他是调用了 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
36History.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);
});
}
}
);
};
先看第一行代码,它调用了 this.router.match 函数:1
2
3
4
5
6
7VueRouter.prototype.match = function match (
raw,
current,
redirectedFrom
) {
return this.matcher.match(raw, current, redirectedFrom)
};
实际上是调用了 this.matcher.match 方法去做匹配,matcher 匹配规则稍后在看。先把 transitionTo 方法看完。
拿到新的路径后,那么接下来就会执行 confirmTransition 方法去做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有成功回调函数和失败回调函数,先来看一下它的定义: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
107History.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);
if (this$1.router.app) {
this$1.router.app.$nextTick(function () {
postEnterCbs.forEach(function (cb) {
cb();
});
});
}
});
});
};
这里定义了 abort 函数,然后判断 route 和 current 是相同路径的话,则直接调用 this.ensureUrl 和 abort 函数抛出错误 和 调用 函数终止回调。
谈后又根据 current.matched 和 route.matched 执行了 resolveQueue 方法解析出 3 个数组队列,分别保存即将前往以及当前的页面的参数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function resolveQueue (
current,
next
) {
var i;
var max = Math.max(current.length, next.length);
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
接下来就来到了导航守卫,守卫是我在狼人杀里面喜欢的一个角色(哈哈)。这里的主要逻辑是在 runQueue 函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function 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);
}
可以看到 是一个典型的异步函数队列执行函数,queue 是一个导航卫士类型的数组,每次根据queue的循环递增取到一个守卫,通过fn 去执行 ,这里的fn 就是刚才的 iterator 函数,定义如下: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
31var 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);
}
};
这里会首先做一个验证,当前待跳转的是否为当前route,然后执行每一个守卫的hook,并传入 route、current 和匿名函数,对应守卫入参时的to, from, next,这里大致就是当执行了匿名函数,会根据一些条件执行 abort 或 next,只有执行 next 的时候,才会前进到下一个导航守卫钩子函数中,这也就是为什么官方文档会说只有执行 next 方法来 resolve 这个钩子函数。
以下还是要多研究
那么最后我们来看 queue 是怎么构造的:
1 | var queue = [].concat( |
按照顺序如下:
1、在失活的组件里调用离开守卫。
2、调用全局的 beforeEach 守卫。
3、在重用的组件里调用 beforeRouteUpdate 守卫
4、在激活的路由配置里调用 beforeEnter。
5、解析异步路由组件。
6、在被激活的组件里调用 beforeRouteEnter。
7、调用全局的 beforeResolve 守卫。
8、调用全局的 afterEach 钩子。
其中6、7步会在runQueue方法的回调中执行:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22runQueue(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);
if (this$1.router.app) {
this$1.router.app.$nextTick(function () {
postEnterCbs.forEach(function (cb) {
cb();
});
});
}
});
});
第八步是在最后执行了 onComplete(route) 后,会执行 this.updateRoute(route) 方法:1
2
3
4
5
6
7
8updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
当用户使用 router.afterEach 注册了一个全局守卫,就会往 router.afterHooks 添加一个钩子函数,这样 this.router.afterHooks 获取的就是用户注册的全局 afterHooks 守卫。
那么至此我们把所有导航守卫的执行分析完毕了,我们知道路由切换除了执行这些钩子函数,从表象上有 2 个地方会发生变化,一个是 url 发生变化,一个是组件发生变化。接下来我们分别介绍这两块的实现原理。
总结
编写 Vue 插件通常需要提供静态的 install 方法,谈后通过 Vue.use(plugin) 时候,就会执行插件的 install 方法。如上Vue-Router 的 install 方法是给每一个组件注入 beforeCreate 和 destoryed 钩子函数,在 beforeCreate 做一些关于Vue-Router 的操作。