如何理解vue-router源码?
vue-router 源码分析
date: 2021-03-26
1. import Vue VueRouter 进来
2. 使用Vue.use(VueRouter) 来进行router的install
-> 使用mixin混入每个组件钩子“beforeCreate”&“destroyed”
-> Object.defineProperty定义拦截“$router”&"$route"
-> Vue.component注册“router-view” & "router-link"
-> 挂载组件上钩子函数(守卫)‘beforeRouterEnter’ & ‘beforeRouteLeave’ & ‘beforeRouteUpdate’
3. 执行new VueRouter里面逻辑
-> 根据用户定义的routes构建匹配器
-> 根据mode进行不同模式进行初始化
4. 若传入了router给实例
-> 则触发组件里面beforeCreated钩子中判断 isDef(this.$options.router)
-> 触发VueRouter的init 初始化方法 且调用transitionTo方法来让路由切换
-> 内部继续调用confirmTransition方法 执行真正切换
/**1. 通过插件方式安装vue-router */
// 源码 src/install.js
// 引入官方组件 router-view router-link
import View from './components/view';
import Link from './components/link';
export let _Vue;
export function install(Vue) {
// TODO:1. 这里作一次 已经安装过的时候 直接返回
if (install.installed && _Vue === Vue) return;
install.installed = true;
_Vue = Vue;
const isDef = v => v !== undefined;
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode;
if (isDef(i) && isDef((i = i.data)) && isDef((i = i.registerRouteInstance))) {
i(vm, callVal);
}
};
// TODO:2. 这个地方通过插件install方法传入Vue实例
// 再通过Vue.mixin 混入beforeCreate&destroyed两个钩子函数
// 这个地方我们需要注意 这里是在我们进行mixin后 那么后面的所有子组件都会有这两个钩子函数 将这两者压入options里面
Vue.mixin({
beforeCreate() {
// 挂载到所有组件中 每次来判断 $options里面是否有router值
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 {
// 若莫得 若其父级存在 则赋值_routerRoot 若均无 则直接给vue实例
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
// TODO: 注册实例
registerInstance(this, this);
},
destroyed() {
registerInstance(this);
},
});
// TODO: 3. 这个地方也很关键 就是在Vue.prototype原型上 设置$router 及$route属性的 getter方法重写
// 所以我们经常在代码中使用this.$router this.$route 这里就是映射到vue-router的实例属性
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot._router;
},
});
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route;
},
});
// TODO: 4. 这个里面通过Vue全局注册了router-view router-link的组件
// 所以我们经常在代码中可以随意使用这两个组件
Vue.component('RouterView', View); // router-view 这个组件是个函数式组件
Vue.component('RouterLink', Link);
const strats = Vue.config.optionMergeStrategies;
// use the same hook merging strategy for route hooks
// TODO: 5. 这个地方是获取到Vue配置项所有方法 并且将路由的组件钩子挂载到Vue上面
// 因此 我们经常在代码里面使用 组件内路由钩子函数
// beforeRouterEnter
// beforeRouteLeave
// beforeRouteUpdate
// 与created 对应的mergeHook一致
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}
/**2. 初始化 */
// 源码 src/index.js
var VueRouter = function VueRouter(options) {
if (options === void 0) options = {
};
this.app = null;
this.apps = [];
this.options = options;
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
// TODO: 这里根据我们传入的routes 路由匹配器 并且在new VueRouter的时候就去做了这件事
this.matcher = createMatcher(options.routes || [], this);
// TODO: 这里设置路由模式 默认为hash模式
var mode = options.mode || 'hash';
// TODO: 这里当设置为history模式时 但是不支持history.pushState时 根据fallback来决定是否回退到hash模式
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
if (this.fallback) {
mode = 'hash';
}
// TODO:如果不是浏览器环境 则直接启用abstract模式路由
if (!inBrowser) {
mode = 'abstract';
}
this.mode = mode;
// TODO: 这里会根据mode的不同 去分别创建对应的history模式实例 后面分别来分析下这些类分别做了些啥
// 这里所有得模式都是基于History基类 下面先了解下History基类
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);
}
}
};
// 2.1 根据路由配置项 来创建匹配器
// [
// {
// path: "/",
// redirect: "/login"
// },
// {
// path: "/home",
// component: () => import("Layout/"),
// redirect: "/",
// children: [
// {
// path: "/",
// name: "home",
// component: () => import("Pages/Home/"),
// meta: {
// index: 1,
// title: "xxx",
// isBackAppRouter: true
// }
// }
// ]
// }
// ]
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 addRoute(parentOrRoute, route) {
var parent = typeof parentOrRoute !== 'object' ? nameMap[parentOrRoute] : undefined;
// $flow-disable-line
createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent);
// add aliases of parent
if (parent) {
createRouteMap(
// $flow-disable-line route is defined if parent is
parent.alias.map(function (alias) {
return {
path: alias, children: [route] };
}),
pathList,
pathMap,
nameMap,
parent
);
}
}
function getRoutes() {
return pathList.map(function (path) {
return pathMap[path];
});
}
function match(raw, currentRoute, redirectedFrom) {
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
.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 redirect(record, location) {
var originalRedirect = record.redirect;
var redirect = typeof originalRedirect === 'function' ? originalRedirect(createRoute(record, location, null, router)) : originalRedirect;
if (typeof redirect === 'string') {
redirect = {
path: redirect };
}
if (!redirect || typeof redirect !== 'object') {
{
warn(false, 'invalid redirect option: ' + JSON.stringify(redirect));
}
return _createRoute(null, location);
}
var re = redirect;
var name = re.name;
var path = re.path;
var query = location.query;
var hash = location.hash;
var params = location.params;
query = re.hasOwnProperty('query') ? re.query : query;
hash = re.hasOwnProperty('hash') ? re.hash : hash;
params = re.hasOwnProperty('params') ? re.params : params;
if (name) {
// resolved named direct
var targetRecord = nameMap[name];
{
assert(targetRecord, 'redirect failed: named route "' + name + '" not found.');
}
return match(
{
_normalized: true,
name: name,
query: query,
hash: hash,
params: params,
},
undefined,
location
);
} else if (path) {
// 1. resolve relative redirect
var rawPath = resolveRecordPath(path, record);
// 2. resolve params
var resolvedPath = fillParams(rawPath, params, 'redirect route with path "' + rawPath + '"');
// 3. rematch with existing query and hash
return match(
{
_normalized: true,
path: resolvedPath,
query: query,
hash: hash,
},
undefined,
location
);
} else {
{
warn(false, 'invalid redirect option: ' + JSON.stringify(redirect));
}
return _createRoute(null, location);
}
}
function alias(record, location, matchAs) {
var aliasedPath = fillParams(matchAs, location.params, 'aliased route with path "' + matchAs + '"');
var aliasedMatch = match({
_normalized: true,
path: aliasedPath,
});
if (aliasedMatch) {
var matched = aliasedMatch.matched;
var aliasedRecord = matched[matched.length - 1];
location.params = aliasedMatch.params;
return _createRoute(aliasedRecord, location);
}
return _createRoute(null, location);
}
function _createRoute(record, location, redirectedFrom) {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location);