vue-router源码浅显分析(一)--Vue.use(VueRouter), new VueRouter(options),HashHistory
1,vue中使用vueRouter需要通过vue.use(vueRouter),众所周知,这里实际上调用了VueRouter的install方法,对Vue进行了扩展,vueRouter的install方法如下:
function install (Vue) {
//如果已经挂载了,跳过
if (install.installed && _Vue === Vue) { return }
//如果未挂载过, installed属性改为true
install.installed = true;
//获取当前vue
_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的beforeCreate钩子和destroyed钩子中新增关于路由的处理
Vue.mixin({
beforeCreate: function beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router; //new VueRouter()
this._router.init(this); //如果Vue实例参数中,有指定的router属性,执行init初始化
//调用vue.util里的方法,通过get和set劫持this._route,router-view实时更新
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);
}
});
//给vue新增$router, $route属性,并指定get方法返回值,$route属性。于是就有了,this.$router和this.$route对象可以使用。
Object.defineProperty(Vue.prototype, '$router', {
//this._routerRoot => this (即vue)
//this._router =>new VueRouter()
get: function get () { return this._routerRoot._router }
});
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
//新增子组件RouterView, RouterLink
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);
var strats = Vue.config.optionMergeStrategies;
// 默认一下组件内导航钩子跟created方法一样
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}
对于配置了路由的vue实例,install方法中做了如下两步操作:
1. this._router = this.$options.router; 获取VueRouter实例(传入的router:router 对象, router = new VueRouter({routes:[]}))
2. this._router.init(this) 重点, 执行VueRouter的init方法
———————————————————-分割线——————————————————-
第一步: new VueRouter({routes:[]})
var VueRouter = function VueRouter (options) {
if ( options === void 0 ) options = {};
this.app = null;
this.apps = [];
this.options = options;
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
//调用createRouteMap方法,遍历routes,执行addRouteRecord(递归)生成的record对象存入
//pathList pathMap nameMap。
//createRouteMap最后 return { pathList: pathList, pathMap: pathMap, nameMap: nameMap }
this.matcher = createMatcher(options.routes || [], this);
var mode = options.mode || 'hash';
//如果当前环境不支持history模式,强制切换到hash模式
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
if (this.fallback) {
mode = 'hash';
}
//如果不是浏览器环境,切换到abstract模式
if (!inBrowser) {
mode = 'abstract';
}
this.mode = mode;
//根据mode值创建不同的实例,生成不同的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));
}
}
};
所以构造函数中做了两件事:
1》对routes数组进行匹配处理;
2》根据mode,创建不同的实例, HTML5History, HashHistory, AbstractHistory
第二步: VueRouter.prototype.init初始化, 主要是根据this.history的原型类型,执行对应的transitionTo方法。
例如:项目中用的hash模式,那么在VueRouter构造方法中会创建出HashHistory实例,init方法中执行HashHistory的transitionTo方法
VueRouter.prototype.init = function init (app /* Vue component instance */) {
var this$1 = this;
"development" !== 'production' && assert(
install.installed,
"not installed. Make sure to call `Vue.use(VueRouter)` " +
"before creating root instance."
);
//app 当前vue
this.apps.push(app);
app.$once('hook:destroyed', function () {
// 监听到destroyed钩子时,从apps数组删除该vue实例
var index = this$1.apps.indexOf(app);
if (index > -1) { this$1.apps.splice(index, 1); }
if (this$1.app === app) { this$1.app = this$1.apps[0] || null; }
});
if (this.app) {
return
}
//this.app 为当前vue
this.app = app;
var history = this.history;
//根据history的原型类型,自行对应的transitionTo方法
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;
});
});
};
-—————-以hash模式 HashHistory实例为例——————-
HashHistory构造方法:HashHistory继承自History
// 在VueRouter构造函数中,this.history = new HashHistory(this, options.base, this.fallback);
//options.base指的是new VueRouter时传入的base属性
var HashHistory = /*@__PURE__*/(function (History$$1) {
//router -> VueRouter
function HashHistory (router, base, fallback) {
History$$1.call(this, router, base);
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash();
}
if ( History$$1 ) HashHistory.__proto__ = History$$1;
HashHistory.prototype = Object.create( History$$1 && History$$1.prototype );
HashHistory.prototype.constructor = HashHistory;
...
return HashHistory;
}(History));
在上面的HashHistory自执行方法中,指定了HashHistory继承自History,并指定了HashHistory一些原型方法:
1.HashHistory.prototype.setupListeners
//是Android 2.或者Android 4.0且是Mobile Safari,不是Chorme, Windows Phone的返回false,否则返回true
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
})();
- 获取用户定义的scrollBehavior
- 给window添加事件监听addEventListener, 如果supportsPushState,支持pushState,监听popstate, 否则监听hashchange;
监听回调里,调用实例的transitionTo
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);
}
});
}
);
注释: getHash() 做了以下工作: 获取hash值和查询参数
1.获取window.location.href
2. 获取‘\#’的索引值index,并截取index+1后的内容
3. 从截取的内容里获取‘?’查询参数的开始索引 seatchIndex
3.1如果没有查询参数, 在获取一次‘\#’索引,确保获取了所有的hash值,并进行decodeURI编码,并返回
3.2如果有查询参数,把hash和查询参数一起decodeURI, 并返回
2.HashHistory.prototype.push(location, onComplete, onAbort)
this.transitionTo(), 跳转到指定路由, 回调用执行pushHash, handleScroll, onComplete
3.HashHistory.prototype.replace
this.transitionTo(), 跳转到指定路由, 回调用执行replaceHash, handleScroll, onComplete
注释: pushHash(path)做了以下工作: 如果supportsPushState, 执行pushState(完整url); 否则window.location.hash= path
**replaceHash(path)**做了以下工作: 如果supportsPushState,执行replaceState(getUrl(path));否则window.location.replace(getUrl(path));
**pushState(url replace) **: 调用window.history方法,如果replace传入true,history.replaceState, 否则history.pushState
4.HashHistory.prototype.go(n)
window.history.go(n);
5.HashHistory.prototype.ensureURL(push)
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current);
}
6.HashHistory.prototype.getCurrentLocation
return getHash()
-—————————————————————————————————————————————————————————————————
综上:
在hashHistory的原型方法,setupListeners(), push(location, onComplete, onAbort), replace(location, onComplete, onAbort),中都调用了this.transitionTo(),而这个方法是在History的原型上定义的。
———————————下面先分析一下History做了什么———————————-
//new HashHistory()时 传入的是router-》 当前VueRouter实例, base 实例初始时传入的base属性
var History = function History (router, base) {
this.router = router; //当前VueRouter实例
this.base = normalizeBase(base);
// start with a route object that stands for "nowhere"
this.current = START; // {path: '/'}
this.pending = null;
this.ready = false;
this.readyCbs = [];
this.readyErrorCbs = [];
this.errorCbs = [];
};
最重要的方法,通用的transitionTo 方法
//transitionTo 方法
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
var this$1 = this;
//重点1
//location => 当前要push或者replace的路由
// this.current => {path: '/'}
var route = this.router.match(location, this.current);
//调用this.confirmTransition方法,完成路由跳转, 重点2
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:
var route = this.router.match(location, this.current);
调用VueRouter实例的match方法,最终调用了createMatcher(options.routes || [], this).match(raw, current, redirectedFrom);
createMatcher方法,返回一个对象,包含match方法属性和addRoutes属性
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] || null ;
...
} else {
record = null
}
return _createRoute(record , location)
}
//重定向或者创建route
function _createRoute (
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)
}
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap);
}
...
return {
match: match,
addRoutes: addRoutes
}
}
match方法,经过各种处理,最终返回的是createRoute()方法的返回值,也就是最终的返回值就是当前要跳转路由的路由对象,也就是 route的结果, 如下代码所示:
//createRoute方法返回值大致如下, 也就是match最终返回的结果格式
createRoute() => return Object.freeze(
{
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query: query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery$$1),
matched: record ? formatMatch(record) : []
}
)
重点2:
route就是上面的当前路由对象
History.prototype.confirmTransition(route, onComplete, onAbort)接受三个参数, 当前route, 完成的回调, 终止的回调
History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
var this$1 = this;
var current = this.current;
//终止路由
var abort = function (err) {
...
onAbort && onAbort(err);
};
//相同的路由,不跳转
if (
isSameRoute(route, current) &&
route.matched.length === current.matched.length
) {
this.ensureURL();
return abort()
}
//处理路由队列,获取当前需要更新的路由,失效的路由,和要激活的路由
var ref = resolveQueue(this.current.matched, route.matched);
var updated = ref.updated;
var deactivated = ref.deactivated;
var activated = ref.activated;
var queue = [].concat(
extractLeaveGuards(deactivated),
this.router.beforeHooks,
extractUpdateHooks(updated),
activated.map(function (m) { return m.beforeEnter; }),
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();
//路由跳转 , 调用实例的replace方法或者push方法, 更改url地址
if (typeof to === 'object' && to.replace) {
this$1.replace(to);
} else {
this$1.push(to);
}
} else {
//执行路由的next方法
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) {
//如果指定了app, 执行app的$nextTick方法
this$1.router.app.$nextTick(function () {
postEnterCbs.forEach(function (cb) { cb(); });
});
}
});
});
};
所以 confirmTransition方法主要做事情是:
通过resolveQueue方法,获取要更新的路由,失活的组件,要激活的组件;
完成url跳转(replace或者push方法)
url改变完成后,执行组件的更新显示(如果组件有指定的vue实例,执行vue实例的$nextTick方法)
总结:
1. 调用VueRouter.install方法,给vue安装路由
2. 安装方法install中,主要做了三件事:
2.1. 给vue的beforeCreate和destroyed钩子中新增关于路由的操作
2.2. 给vue实例定义$router(当前路由实例new VueRouter) $route(当前路由对象)属性
2.3. 给vue新增子组件 RouterView和RouterLink
3. beforeCreate钩子中,获取vue实例参数中配置的router,即new VueRouter实例,执行实例的init方法;
4. new VueRouter时, 根据传入的routes创建this.Matcher用于匹配路由,根据传入的mode等参数,新增对应的实例(HTML5History, HashHistory, AbstractHistory)赋值给this.history
5.VueRouter.prototype.init方法中,主要做了两件事,
5.1 根据this.history的原型类型,分别执行对应的transitionTo方法,其中HashHistory实例需要先执行setupListeners()方法
5.2 调用history.listen()方法,获取当前route路由对象,this.$route的值
6. HTML5History, HashHistory, AbstractHistory实例都继承自History,transitionTo定义在History的原型上。
HTML5History监听了window对象的popstate事件
HashHistory监听了window对象的hashchange事件或者popstate
还没有评论,来说两句吧...